@apify/ui-library 1.141.3 → 1.145.2-featverifieddeveloperbadge-bd59c6.13

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 (98) hide show
  1. package/README.md +90 -19
  2. package/dist/src/components/box/box.d.ts +15 -14
  3. package/dist/src/components/box/box.d.ts.map +1 -1
  4. package/dist/src/components/box/box.js +10 -33
  5. package/dist/src/components/box/box.js.map +1 -1
  6. package/dist/src/components/breadcrumb/breadcrumb.d.ts.map +1 -1
  7. package/dist/src/components/breadcrumb/breadcrumb.js +1 -0
  8. package/dist/src/components/breadcrumb/breadcrumb.js.map +1 -1
  9. package/dist/src/components/card/card.d.ts.map +1 -1
  10. package/dist/src/components/card/card.js +1 -0
  11. package/dist/src/components/card/card.js.map +1 -1
  12. package/dist/src/components/chip/chip.d.ts.map +1 -1
  13. package/dist/src/components/chip/chip.js +2 -0
  14. package/dist/src/components/chip/chip.js.map +1 -1
  15. package/dist/src/components/code/code_block/code_block.styled.d.ts.map +1 -1
  16. package/dist/src/components/code/code_block/code_block.styled.js +1 -2
  17. package/dist/src/components/code/code_block/code_block.styled.js.map +1 -1
  18. package/dist/src/components/code/one_light_theme.d.ts.map +1 -1
  19. package/dist/src/components/code/one_light_theme.js +1 -0
  20. package/dist/src/components/code/one_light_theme.js.map +1 -1
  21. package/dist/src/components/dropdown/dropdown.context.d.ts +6 -0
  22. package/dist/src/components/dropdown/dropdown.context.d.ts.map +1 -0
  23. package/dist/src/components/dropdown/dropdown.context.js +10 -0
  24. package/dist/src/components/dropdown/dropdown.context.js.map +1 -0
  25. package/dist/src/components/dropdown/dropdown.d.ts +48 -0
  26. package/dist/src/components/dropdown/dropdown.d.ts.map +1 -0
  27. package/dist/src/components/dropdown/dropdown.js +65 -0
  28. package/dist/src/components/dropdown/dropdown.js.map +1 -0
  29. package/dist/src/components/dropdown/dropdown.styled.d.ts +35 -0
  30. package/dist/src/components/dropdown/dropdown.styled.d.ts.map +1 -0
  31. package/dist/src/components/dropdown/dropdown.styled.js +241 -0
  32. package/dist/src/components/dropdown/dropdown.styled.js.map +1 -0
  33. package/dist/src/components/dropdown/dropdown.types.d.ts +17 -0
  34. package/dist/src/components/dropdown/dropdown.types.d.ts.map +1 -0
  35. package/dist/src/components/dropdown/dropdown.types.js +2 -0
  36. package/dist/src/components/dropdown/dropdown.types.js.map +1 -0
  37. package/dist/src/components/dropdown/dropdown_button.d.ts +10 -0
  38. package/dist/src/components/dropdown/dropdown_button.d.ts.map +1 -0
  39. package/dist/src/components/dropdown/dropdown_button.js +16 -0
  40. package/dist/src/components/dropdown/dropdown_button.js.map +1 -0
  41. package/dist/src/components/dropdown/dropdown_root.d.ts +11 -0
  42. package/dist/src/components/dropdown/dropdown_root.d.ts.map +1 -0
  43. package/dist/src/components/dropdown/dropdown_root.js +19 -0
  44. package/dist/src/components/dropdown/dropdown_root.js.map +1 -0
  45. package/dist/src/components/dropdown/dropdown_shell.d.ts +15 -0
  46. package/dist/src/components/dropdown/dropdown_shell.d.ts.map +1 -0
  47. package/dist/src/components/dropdown/dropdown_shell.js +69 -0
  48. package/dist/src/components/dropdown/dropdown_shell.js.map +1 -0
  49. package/dist/src/components/dropdown/index.d.ts +7 -0
  50. package/dist/src/components/dropdown/index.d.ts.map +1 -0
  51. package/dist/src/components/dropdown/index.js +5 -0
  52. package/dist/src/components/dropdown/index.js.map +1 -0
  53. package/dist/src/components/index.d.ts +1 -0
  54. package/dist/src/components/index.d.ts.map +1 -1
  55. package/dist/src/components/index.js +1 -0
  56. package/dist/src/components/index.js.map +1 -1
  57. package/dist/src/components/message/message.d.ts +2 -0
  58. package/dist/src/components/message/message.d.ts.map +1 -1
  59. package/dist/src/components/message/message.js +2 -0
  60. package/dist/src/components/message/message.js.map +1 -1
  61. package/dist/src/components/pagination/pagination.d.ts.map +1 -1
  62. package/dist/src/components/pagination/pagination.js +1 -0
  63. package/dist/src/components/pagination/pagination.js.map +1 -1
  64. package/dist/src/components/tabs/tab.js +1 -1
  65. package/dist/src/components/tabs/tab.js.map +1 -1
  66. package/dist/src/utils/css_utils.d.ts +17 -0
  67. package/dist/src/utils/css_utils.d.ts.map +1 -1
  68. package/dist/src/utils/css_utils.js +54 -0
  69. package/dist/src/utils/css_utils.js.map +1 -1
  70. package/dist/tsconfig.build.tsbuildinfo +1 -1
  71. package/package.json +10 -5
  72. package/src/components/box/box.stories.tsx +18 -0
  73. package/src/components/box/box.tsx +31 -62
  74. package/src/components/breadcrumb/breadcrumb.tsx +1 -0
  75. package/src/components/card/card.tsx +1 -0
  76. package/src/components/chip/chip.tsx +2 -0
  77. package/src/components/code/code_block/code_block.styled.tsx +1 -2
  78. package/src/components/code/one_light_theme.ts +1 -0
  79. package/src/components/dropdown/dropdown.context.tsx +14 -0
  80. package/src/components/dropdown/dropdown.stories.tsx +343 -0
  81. package/src/components/dropdown/dropdown.styled.ts +265 -0
  82. package/src/components/dropdown/dropdown.tsx +259 -0
  83. package/src/components/dropdown/dropdown.types.ts +18 -0
  84. package/src/components/dropdown/dropdown_button.tsx +45 -0
  85. package/src/components/dropdown/dropdown_root.tsx +41 -0
  86. package/src/components/dropdown/dropdown_shell.tsx +139 -0
  87. package/src/components/dropdown/index.ts +6 -0
  88. package/src/components/index.ts +1 -0
  89. package/src/components/message/message.stories.jsx +1 -0
  90. package/src/components/message/message.tsx +2 -0
  91. package/src/components/pagination/pagination.tsx +1 -0
  92. package/src/components/tabs/tab.tsx +1 -1
  93. package/src/design_system/colors/build_color_tokens.js +14 -0
  94. package/src/design_system/colors/colors.stories.tsx +395 -0
  95. package/src/utils/css_utils.ts +79 -0
  96. package/style/colors/tokens.dark.css +149 -0
  97. package/style/colors/tokens.light.css +149 -0
  98. package/src/design_system/colors/Colors.mdx +0 -50
@@ -0,0 +1,139 @@
1
+ import {
2
+ autoUpdate,
3
+ flip,
4
+ offset as offsetMiddleware,
5
+ type Placement,
6
+ shift,
7
+ size,
8
+ useFloating,
9
+ } from '@floating-ui/react';
10
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
11
+ import { type ReactNode, type RefObject, useCallback, useLayoutEffect } from 'react';
12
+
13
+ import { theme } from '../../design_system/theme.js';
14
+ import { DropdownContext } from './dropdown.context.js';
15
+ import { ScrollableContent, StyledContent } from './dropdown.styled.js';
16
+ import type { BaseDropdownContentProps } from './dropdown.types.js';
17
+
18
+ export type DropdownShellProps = BaseDropdownContentProps & {
19
+ children: ReactNode;
20
+ isOpen: boolean;
21
+ onIsOpenChange: (open: boolean) => void;
22
+ triggerRef: RefObject<HTMLElement | null>;
23
+ strategy?: 'absolute' | 'fixed';
24
+ };
25
+
26
+ /**
27
+ * DropdownShell positions the dropdown relative to a custom trigger element via Floating UI.
28
+ * Use it when you have a custom trigger (not a Button) and need explicit ref-based positioning.
29
+ */
30
+ export const DropdownShell = ({
31
+ isOpen,
32
+ onIsOpenChange,
33
+ children,
34
+ closeOnSelect = true,
35
+ contentProps,
36
+ width,
37
+ height,
38
+ triggerRef,
39
+ strategy = 'absolute',
40
+ zIndex,
41
+ }: DropdownShellProps) => {
42
+ const handleSelectAndClose = useCallback(
43
+ (event?: Event) => {
44
+ if (closeOnSelect) onIsOpenChange(false);
45
+ if (event) event.preventDefault();
46
+ },
47
+ [closeOnSelect, onIsOpenChange],
48
+ );
49
+
50
+ const side = contentProps?.side || 'bottom';
51
+ const align = contentProps?.align || 'start';
52
+ const sideOffset = contentProps?.sideOffset || 0;
53
+ const alignOffset = contentProps?.alignOffset || 0;
54
+
55
+ const placement: Placement = align === 'center' ? (side as Placement) : (`${side}-${align}` as Placement);
56
+
57
+ const floatingMiddleware = [
58
+ shift(),
59
+ flip(),
60
+ size({
61
+ apply({ availableHeight, elements }) {
62
+ Object.assign(elements.floating.style, {
63
+ maxHeight: `calc(${availableHeight}px - ${theme.space.space8})`,
64
+ });
65
+ },
66
+ }),
67
+ ];
68
+
69
+ if (sideOffset || alignOffset) {
70
+ floatingMiddleware.push(offsetMiddleware({ mainAxis: sideOffset, crossAxis: alignOffset }));
71
+ }
72
+
73
+ const {
74
+ x,
75
+ y,
76
+ refs: { setReference, setFloating },
77
+ strategy: floatingStrategy,
78
+ } = useFloating({
79
+ placement,
80
+ strategy,
81
+ middleware: floatingMiddleware,
82
+ whileElementsMounted: autoUpdate,
83
+ });
84
+
85
+ useLayoutEffect(() => {
86
+ if (triggerRef?.current) {
87
+ setReference(triggerRef.current);
88
+ }
89
+ }, [triggerRef, setReference]);
90
+
91
+ const handlePointerDownOutside = useCallback(
92
+ (event: any) => {
93
+ const isInsideTrigger = triggerRef?.current?.contains(event.detail.originalEvent.target as Node);
94
+ if (!isInsideTrigger) {
95
+ onIsOpenChange(false);
96
+ }
97
+ },
98
+ [triggerRef, onIsOpenChange],
99
+ );
100
+
101
+ const shouldShowDropdown = isOpen && x !== null && y !== null;
102
+
103
+ const setFloatingRef = useCallback(
104
+ (element: HTMLElement | null) => {
105
+ setFloating(element);
106
+ },
107
+ [setFloating],
108
+ );
109
+
110
+ if (!shouldShowDropdown) return null;
111
+
112
+ const fullscreenContainer = document.fullscreenElement as HTMLElement | null;
113
+
114
+ return (
115
+ <DropdownContext.Provider value={{ handleSelectAndClose }}>
116
+ <DropdownMenu.Root open={true} modal={false}>
117
+ <DropdownMenu.Portal container={fullscreenContainer ?? undefined}>
118
+ <StyledContent
119
+ ref={setFloatingRef}
120
+ $width={width}
121
+ $height={height}
122
+ $zIndex={zIndex}
123
+ data-radix-dropdown-content
124
+ onPointerDownOutside={handlePointerDownOutside}
125
+ style={{
126
+ position: floatingStrategy,
127
+ top: y ?? '',
128
+ left: x ?? '',
129
+ zIndex: zIndex ?? 100,
130
+ }}
131
+ {...contentProps}
132
+ >
133
+ <ScrollableContent>{children}</ScrollableContent>
134
+ </StyledContent>
135
+ </DropdownMenu.Portal>
136
+ </DropdownMenu.Root>
137
+ </DropdownContext.Provider>
138
+ );
139
+ };
@@ -0,0 +1,6 @@
1
+ export { Dropdown } from './dropdown.js';
2
+ export { DropdownButton } from './dropdown_button.js';
3
+ export { DropdownShell } from './dropdown_shell.js';
4
+ export type { DropdownShellProps } from './dropdown_shell.js';
5
+ export { DROPDOWN_ELEMENT_CLASSES } from './dropdown.styled.js';
6
+ export type { BaseDropdownItemProps, BaseDropdownContentProps } from './dropdown.types.js';
@@ -37,3 +37,4 @@ export * from './select';
37
37
  export * from './switch';
38
38
  export * from './table';
39
39
  export * from './breadcrumb';
40
+ export * from './dropdown';
@@ -12,6 +12,7 @@ const MESSAGE_TYPES = ['info', 'warning', 'danger', 'success'];
12
12
 
13
13
  export default {
14
14
  title: 'UI-Library/Message (aka Banner)',
15
+ tags: ['deprecated'],
15
16
  component: Message,
16
17
  argTypes: {
17
18
  type: {
@@ -158,6 +158,8 @@ type MessageProps = BoxProps & {
158
158
 
159
159
  /**
160
160
  * Component used to display larger messages that are part of the content of the application.
161
+ * @deprecated Use `Alert` instead once it is available.
162
+ * @see https://www.notion.so/apify/Design-system-roadmap-2026-326f39950a2280d4b81bec00ebc9cf44
161
163
  */
162
164
  export const Message: React.FC<MessageProps> = ({
163
165
  className,
@@ -15,6 +15,7 @@ const PaginationButtonBase = styled(Button).attrs({ variant: 'tertiary' })`
15
15
  padding: 0;
16
16
  height: 28px;
17
17
  min-width: 28px;
18
+ /* TODO: (typography-partial) font-weight override without paired font-size/line-height — revisit to confirm intended typography token. */
18
19
  font-weight: 400 !important; /* default for button is medium, force regular to all page selection buttons */
19
20
 
20
21
  &:disabled {
@@ -199,11 +199,11 @@ export const Tab = ({
199
199
  const href = typeof to === 'string' ? to : createPath(to);
200
200
  return (
201
201
  <TabWrapper
202
+ data-test="tab"
202
203
  {...props}
203
204
  id={id}
204
205
  to={to}
205
206
  role="tab"
206
- data-test="tab"
207
207
  data-test-url={href}
208
208
  className={clsx(className, { active, disabled })}
209
209
  onClick={onSelect ? (event) => onSelect({ id, href, event }) : undefined}
@@ -45,6 +45,15 @@ async function buildTheme(theme) {
45
45
  },
46
46
  });
47
47
 
48
+ // Format to generate a plain CSS file with tokens inside a :root block
49
+ StyleDictionary.registerFormat({
50
+ name: 'css/custom-properties',
51
+ format: ({ dictionary }) => {
52
+ const lines = dictionary.allTokens.map((token) => ` --${token.name}: ${token.$value};`);
53
+ return `${DO_NOT_EXPORT_COMMENT}\n:root {\n${lines.join('\n')}\n}\n`;
54
+ },
55
+ });
56
+
48
57
  // Format to generate typescript file that exports CSS variables as a string
49
58
  // Example: export const tokens = `--color-neutral-text: #000; --color-primary: #fff;`
50
59
  StyleDictionary.registerFormat({
@@ -165,6 +174,11 @@ async function buildTheme(theme) {
165
174
  mapName: 'tokens',
166
175
  filter: 'only-palette',
167
176
  },
177
+ {
178
+ destination: `tokens.${theme}.css`,
179
+ format: 'css/custom-properties',
180
+ filter: 'only-colors',
181
+ },
168
182
  ],
169
183
  },
170
184
  },
@@ -0,0 +1,395 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import type { CSSProperties, ReactNode } from 'react';
3
+
4
+ import { theme } from '../theme.js';
5
+ import { cssColorsVariablesDark, cssColorsVariablesLight, darkTheme, lightTheme } from './index.js';
6
+
7
+ type ColorToken = {
8
+ name: string;
9
+ value: string;
10
+ variable: string;
11
+ };
12
+
13
+ type PaletteColor = {
14
+ name: string;
15
+ shade: string;
16
+ value: string;
17
+ };
18
+
19
+ type SemanticColorSection = {
20
+ title: string;
21
+ prefix: string;
22
+ description: string;
23
+ };
24
+
25
+ const basePaletteFamilies = ['blue', 'green', 'yellow', 'red'];
26
+ const neutralPaletteFamilies = ['neutral'];
27
+ const decorativePaletteFamilies = [
28
+ 'coral',
29
+ 'lavender',
30
+ 'bamboo',
31
+ 'rose',
32
+ 'buttercup',
33
+ 'paprika',
34
+ 'teal',
35
+ 'indigo',
36
+ 'slate',
37
+ ];
38
+ const brandPaletteFamilies = ['magenta', 'mondo', 'lime', 'meteorite', 'logo'];
39
+ const semanticColorSections: SemanticColorSection[] = [
40
+ {
41
+ title: 'Neutral',
42
+ prefix: 'neutral',
43
+ description: 'Foundational semantic colors for text, icons, surfaces, borders, and controls.',
44
+ },
45
+ {
46
+ title: 'Primary',
47
+ prefix: 'primary',
48
+ description: 'Primary brand actions, selected states, interactive text, and blue-tinted surfaces.',
49
+ },
50
+ {
51
+ title: 'PrimaryBlack',
52
+ prefix: 'primaryBlack',
53
+ description: 'High-emphasis black/white primary action tokens.',
54
+ },
55
+ {
56
+ title: 'Success',
57
+ prefix: 'success',
58
+ description: 'Positive status colors for success states, chips, borders, and actions.',
59
+ },
60
+ {
61
+ title: 'Warning',
62
+ prefix: 'warning',
63
+ description: 'Cautionary status colors for warning states, chips, borders, and fields.',
64
+ },
65
+ {
66
+ title: 'Danger',
67
+ prefix: 'danger',
68
+ description: 'Critical status colors for destructive actions, errors, chips, and borders.',
69
+ },
70
+ {
71
+ title: 'Special',
72
+ prefix: 'special',
73
+ description: 'Special-purpose semantic colors for plans and highlights.',
74
+ },
75
+ ];
76
+
77
+ const styles = {
78
+ page: { display: 'flex', flexDirection: 'column', gap: 40, padding: 24, color: theme.color.neutral.text },
79
+ section: { display: 'flex', flexDirection: 'column', gap: 20 },
80
+ paletteGroup: {
81
+ display: 'flex',
82
+ flexDirection: 'column',
83
+ gap: 20,
84
+ padding: 20,
85
+ border: `1px solid ${theme.color.neutral.border}`,
86
+ borderRadius: 10,
87
+ background: lightTheme.neutral0,
88
+ color: lightTheme.neutral850,
89
+ },
90
+ paletteGroupHeader: { display: 'flex', flexDirection: 'column', gap: 4 },
91
+ paletteGroupTitle: { margin: 0, fontSize: 20, fontWeight: 700, lineHeight: 1.2 },
92
+ paletteGroupDescription: { margin: 0, color: lightTheme.neutral500, fontSize: 12, lineHeight: 1.4 },
93
+ sectionTitle: {
94
+ margin: 0,
95
+ paddingBottom: 10,
96
+ borderBottom: `1px solid ${theme.color.neutral.border}`,
97
+ fontSize: 28,
98
+ fontWeight: 700,
99
+ lineHeight: 1.2,
100
+ },
101
+ themeBlock: { display: 'flex', flexDirection: 'column', gap: 20 },
102
+ themeBlockDark: { padding: 24, borderRadius: 8, background: darkTheme.neutral900, color: darkTheme.neutral50 },
103
+ themeTitle: { margin: 0, fontSize: 22, fontWeight: 700, lineHeight: 1.2 },
104
+ neutralThemeGrid: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 32 },
105
+ paletteGrid: { display: 'grid', gridTemplateColumns: 'repeat(4, minmax(160px, 1fr))', gap: '28px 40px' },
106
+ paletteColumn: { display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 },
107
+ paletteColumnTitle: { margin: 0, fontSize: 18, fontWeight: 700, lineHeight: 1.2 },
108
+ paletteRow: { display: 'grid', gridTemplateColumns: '92px minmax(0, 1fr)', gap: 10, alignItems: 'center' },
109
+ paletteSwatch: { height: 44, borderRadius: 8 },
110
+ paletteMeta: { display: 'flex', flexDirection: 'column', minWidth: 0, fontSize: 12, lineHeight: 1.25 },
111
+ paletteShade: { fontSize: 16, fontWeight: 700 },
112
+ semanticGrid: { display: 'grid', gridTemplateColumns: '1fr 1fr' },
113
+ semanticPanel: {
114
+ display: 'flex',
115
+ flexDirection: 'column',
116
+ gap: 10,
117
+ padding: 24,
118
+ background: lightTheme.neutral0,
119
+ color: lightTheme.neutral850,
120
+ },
121
+ semanticPanelDark: { borderRadius: 8, background: darkTheme.neutral900, color: darkTheme.neutral50 },
122
+ semanticTokenRow: { display: 'grid', gridTemplateColumns: '64px minmax(0, 1fr)', gap: 10, alignItems: 'center' },
123
+ semanticTokenSwatch: { height: 28, borderRadius: 6 },
124
+ tokenMeta: {
125
+ display: 'flex',
126
+ flexDirection: 'column',
127
+ gap: 2,
128
+ marginTop: 6,
129
+ minWidth: 0,
130
+ fontSize: 11,
131
+ lineHeight: 1.25,
132
+ },
133
+ paletteName: { overflow: 'hidden', fontWeight: 700, textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
134
+ colorValue: {
135
+ overflow: 'hidden',
136
+ color: lightTheme.neutral500,
137
+ fontFamily: 'monospace',
138
+ textOverflow: 'ellipsis',
139
+ whiteSpace: 'nowrap',
140
+ },
141
+ } satisfies Record<string, CSSProperties>;
142
+
143
+ const parseColorTokens = (tokens: string): ColorToken[] =>
144
+ tokens
145
+ .trim()
146
+ .split('\n')
147
+ .map((line) => line.trim().match(/^(--color-([\w-]+)):\s*(.+);$/))
148
+ .filter((match): match is RegExpMatchArray => Boolean(match))
149
+ .map(([, variable, path, value]) => ({
150
+ name: path.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase()),
151
+ value,
152
+ variable,
153
+ }));
154
+
155
+ const lightColors = parseColorTokens(cssColorsVariablesLight);
156
+ const darkColors = parseColorTokens(cssColorsVariablesDark);
157
+
158
+ const formatLabel = (name: string) =>
159
+ name
160
+ .replace(/([a-z])([A-Z0-9])/g, '$1 $2')
161
+ .replace(/^./, (char) => char.toUpperCase())
162
+ .toLowerCase()
163
+ .replace(/^./, (char) => char.toUpperCase());
164
+
165
+ const isColorInPrefix = (name: string, prefix: string) =>
166
+ name === prefix || name.startsWith(`${prefix}${name[prefix.length]?.toUpperCase() ?? ''}`);
167
+
168
+ const getColorDisplayName = (name: string, prefix: string) => {
169
+ const displayName = name.slice(prefix.length);
170
+
171
+ return formatLabel(displayName ? `${displayName[0].toLowerCase()}${displayName.slice(1)}` : name);
172
+ };
173
+
174
+ const getPaletteNameByValue = (value: string, palette: Record<string, string>) => {
175
+ const paletteColor = Object.entries(palette).find(
176
+ ([, paletteValue]) => paletteValue.toLowerCase() === value.toLowerCase(),
177
+ );
178
+
179
+ return paletteColor ? formatLabel(paletteColor[0]) : 'Color';
180
+ };
181
+
182
+ const getColorSectionItems = (colors: ColorToken[], prefix: string) =>
183
+ colors
184
+ .filter(({ name }) => isColorInPrefix(name, prefix))
185
+ .sort((colorA, colorB) => colorA.name.localeCompare(colorB.name));
186
+
187
+ const getPaletteFamily = (name: string) => name.match(/^[a-z]+/)?.[0] ?? name;
188
+
189
+ const getPaletteShade = (name: string) => {
190
+ const shade = name.slice(getPaletteFamily(name).length);
191
+
192
+ return shade || name;
193
+ };
194
+
195
+ const getPaletteGroups = (palette: Record<string, string>) =>
196
+ Object.entries(palette).reduce<Record<string, PaletteColor[]>>((groups, [name, value]) => {
197
+ const family = getPaletteFamily(name);
198
+ const familyColors = [...(groups[family] ?? []), { name, shade: getPaletteShade(name), value }].sort(
199
+ (colorA, colorB) => colorA.name.localeCompare(colorB.name, undefined, { numeric: true }),
200
+ );
201
+
202
+ return { ...groups, [family]: familyColors };
203
+ }, {});
204
+
205
+ const getPaletteGroupsByFamily = (palette: Record<string, string>, families: string[]) => {
206
+ const groups = getPaletteGroups(palette);
207
+
208
+ return families.flatMap((family) => (groups[family] ? [[family, groups[family]] as const] : []));
209
+ };
210
+
211
+ const Section = ({ title, children }: { title: string; children?: ReactNode }) => (
212
+ <section style={styles.section}>
213
+ <h2 style={styles.sectionTitle}>{title}</h2>
214
+ {children}
215
+ </section>
216
+ );
217
+
218
+ const PaletteColumn = ({ family, colors }: { family: string; colors: PaletteColor[] }) => (
219
+ <div style={styles.paletteColumn}>
220
+ <h3 style={styles.paletteColumnTitle}>{formatLabel(family)}</h3>
221
+ {colors.map(({ name, shade, value }) => (
222
+ <div key={name} style={styles.paletteRow}>
223
+ <div style={{ ...styles.paletteSwatch, backgroundColor: value }} />
224
+ <div style={styles.paletteMeta}>
225
+ <span style={styles.paletteShade}>{shade}</span>
226
+ <span style={styles.colorValue} title={value}>
227
+ {value}
228
+ </span>
229
+ </div>
230
+ </div>
231
+ ))}
232
+ </div>
233
+ );
234
+
235
+ const PaletteFamilyGrid = ({ families, palette }: { families: string[]; palette: Record<string, string> }) => (
236
+ <div style={styles.paletteGrid}>
237
+ {getPaletteGroupsByFamily(palette, families).map(([family, colors]) => (
238
+ <PaletteColumn key={family} family={family} colors={colors} />
239
+ ))}
240
+ </div>
241
+ );
242
+
243
+ const PaletteTheme = ({
244
+ families,
245
+ isDark = false,
246
+ palette,
247
+ title,
248
+ }: {
249
+ families: string[];
250
+ isDark?: boolean;
251
+ palette: Record<string, string>;
252
+ title: string;
253
+ }) => (
254
+ <div style={isDark ? { ...styles.themeBlock, ...styles.themeBlockDark } : styles.themeBlock}>
255
+ <h3 style={styles.themeTitle}>{title}</h3>
256
+ <PaletteFamilyGrid families={families} palette={palette} />
257
+ </div>
258
+ );
259
+
260
+ const PaletteGroup = ({
261
+ children,
262
+ description,
263
+ title,
264
+ }: {
265
+ children?: ReactNode;
266
+ description?: string;
267
+ title: string;
268
+ }) => (
269
+ <div style={styles.paletteGroup}>
270
+ <div style={styles.paletteGroupHeader}>
271
+ <h3 style={styles.paletteGroupTitle}>{title}</h3>
272
+ {description ? <p style={styles.paletteGroupDescription}>{description}</p> : null}
273
+ </div>
274
+ {children}
275
+ </div>
276
+ );
277
+
278
+ const ColorTokenRow = ({
279
+ color,
280
+ palette,
281
+ name,
282
+ }: {
283
+ color: ColorToken;
284
+ palette: Record<string, string>;
285
+ name: string;
286
+ }) => {
287
+ const paletteName = getPaletteNameByValue(color.value, palette);
288
+
289
+ return (
290
+ <div style={styles.semanticTokenRow}>
291
+ <div style={{ ...styles.semanticTokenSwatch, backgroundColor: color.value }} />
292
+ <div style={styles.tokenMeta}>
293
+ <span style={styles.paletteShade}>{name}</span>
294
+ <span style={styles.paletteName} title={paletteName}>
295
+ {paletteName}
296
+ </span>
297
+ <span style={styles.colorValue} title={color.value}>
298
+ {color.value}
299
+ </span>
300
+ </div>
301
+ </div>
302
+ );
303
+ };
304
+
305
+ const PaletteSection = () => (
306
+ <Section title="Palette">
307
+ <PaletteGroup
308
+ title="Base palette"
309
+ description="Base palette scales for product UI. Use semantic colors for implementation."
310
+ >
311
+ <PaletteTheme families={basePaletteFamilies} palette={lightTheme} title="Light" />
312
+ <PaletteTheme families={basePaletteFamilies} isDark palette={darkTheme} title="Dark" />
313
+ </PaletteGroup>
314
+ <PaletteGroup title="Neutrals" description="Neutral scales for text, surfaces, borders, and structure.">
315
+ <div style={styles.neutralThemeGrid}>
316
+ <PaletteTheme families={neutralPaletteFamilies} palette={lightTheme} title="Light" />
317
+ <PaletteTheme families={neutralPaletteFamilies} isDark palette={darkTheme} title="Dark" />
318
+ </div>
319
+ </PaletteGroup>
320
+ <PaletteGroup
321
+ title="Decorative"
322
+ description="Accent colors for charts and illustrations. Do not use for semantic states."
323
+ >
324
+ <PaletteTheme families={decorativePaletteFamilies} palette={lightTheme} title="Light" />
325
+ <PaletteTheme families={decorativePaletteFamilies} isDark palette={darkTheme} title="Dark" />
326
+ </PaletteGroup>
327
+ <PaletteGroup
328
+ title="Brand"
329
+ description="Brand accent colors for charts and illustrations. Do not use for semantic states."
330
+ >
331
+ <PaletteFamilyGrid families={brandPaletteFamilies} palette={lightTheme} />
332
+ </PaletteGroup>
333
+ </Section>
334
+ );
335
+
336
+ const SemanticTokenPanel = ({
337
+ colors,
338
+ isDark = false,
339
+ palette,
340
+ prefix,
341
+ title,
342
+ }: {
343
+ colors: ColorToken[];
344
+ isDark?: boolean;
345
+ palette: Record<string, string>;
346
+ prefix: string;
347
+ title: string;
348
+ }) => (
349
+ <div style={isDark ? { ...styles.semanticPanel, ...styles.semanticPanelDark } : styles.semanticPanel}>
350
+ <h3 style={styles.paletteColumnTitle}>{title}</h3>
351
+ {getColorSectionItems(colors, prefix).map((color) => (
352
+ <ColorTokenRow
353
+ key={color.variable}
354
+ color={color}
355
+ name={getColorDisplayName(color.name, prefix)}
356
+ palette={palette}
357
+ />
358
+ ))}
359
+ </div>
360
+ );
361
+
362
+ const SemanticTokenGroup = ({ description, prefix, title }: SemanticColorSection) => (
363
+ <PaletteGroup description={description} title={title}>
364
+ <div style={styles.semanticGrid}>
365
+ <SemanticTokenPanel colors={lightColors} palette={lightTheme} prefix={prefix} title="Light" />
366
+ <SemanticTokenPanel colors={darkColors} isDark palette={darkTheme} prefix={prefix} title="Dark" />
367
+ </div>
368
+ </PaletteGroup>
369
+ );
370
+
371
+ const SemanticSection = () => (
372
+ <Section title="Semantic">
373
+ {semanticColorSections.map((section) => (
374
+ <SemanticTokenGroup key={section.prefix} {...section} />
375
+ ))}
376
+ </Section>
377
+ );
378
+
379
+ const ColorsShowcase = () => (
380
+ <div style={styles.page}>
381
+ <PaletteSection />
382
+ <SemanticSection />
383
+ </div>
384
+ );
385
+
386
+ export default {
387
+ title: 'Design Tokens/Colors',
388
+ component: ColorsShowcase,
389
+ } as Meta<typeof ColorsShowcase>;
390
+
391
+ type Story = StoryObj<typeof ColorsShowcase>;
392
+
393
+ export const Default: Story = {
394
+ render: () => <ColorsShowcase />,
395
+ };