@soyfri/shared-library 2.0.0-beta.2 → 2.0.0-beta.4

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 (187) hide show
  1. package/.dockerignore +8 -0
  2. package/.github/workflows/publish.yml +107 -0
  3. package/.prettierrc +3 -0
  4. package/.storybook/main.ts +19 -0
  5. package/.storybook/preview.ts +14 -0
  6. package/.storybook/vitest.setup.ts +9 -0
  7. package/Dockerfile +37 -0
  8. package/build.js +102 -0
  9. package/chromatic.config.json +5 -0
  10. package/cleanDirectories.js +40 -0
  11. package/dist/README.md +243 -0
  12. package/dist/components/Icon/Icon.js +1 -1
  13. package/dist/components/Table/Table.js +1 -1
  14. package/dist/index.cjs +24 -0
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +7 -1
  17. package/dist/mui.d.ts +1 -0
  18. package/dist/package.json +197 -0
  19. package/package.json +4 -32
  20. package/rollup.config.cjs +87 -0
  21. package/src/components/ActionMenu/ActionMenu.stories.tsx +230 -0
  22. package/src/components/ActionMenu/ActionMenu.tsx +174 -0
  23. package/src/components/ActionMenu/index.ts +2 -0
  24. package/src/components/AppBar/AppBar.stories.tsx +272 -0
  25. package/src/components/AppBar/AppBar.sx.ts +32 -0
  26. package/src/components/AppBar/AppBar.tsx +123 -0
  27. package/src/components/AppBar/AppBarBrand.tsx +120 -0
  28. package/src/components/AppBar/AppBarContext.ts +25 -0
  29. package/src/components/AppBar/AppBarMenuToggle.tsx +90 -0
  30. package/src/components/AppBar/AppBarUserMenu.tsx +217 -0
  31. package/src/components/AppBar/index.ts +25 -0
  32. package/src/components/Autocomplete/Autocomplete.definitions.ts +477 -0
  33. package/src/components/Autocomplete/Autocomplete.helpers.ts +60 -0
  34. package/src/components/Autocomplete/Autocomplete.stories.tsx +748 -0
  35. package/src/components/Autocomplete/Autocomplete.sx.ts +30 -0
  36. package/src/components/Autocomplete/Autocomplete.tsx +361 -0
  37. package/src/components/Autocomplete/Autocomplete.types.ts +13 -0
  38. package/src/components/Autocomplete/_parts/AutocompleteChips.tsx +55 -0
  39. package/src/components/Autocomplete/_parts/AutocompleteLoader.tsx +17 -0
  40. package/src/components/Autocomplete/_parts/AutocompleteOption.tsx +31 -0
  41. package/src/components/Autocomplete/index.ts +12 -0
  42. package/src/components/Avatar/Avatar.definitions.ts +162 -0
  43. package/src/components/Avatar/Avatar.stories.tsx +258 -0
  44. package/src/components/Avatar/Avatar.tsx +206 -0
  45. package/src/components/Avatar/index.ts +1 -0
  46. package/src/components/Button/Button.definition.ts +97 -0
  47. package/src/components/Button/Button.stories.tsx +285 -0
  48. package/src/components/Button/Button.tsx +67 -0
  49. package/src/components/Button/index.ts +1 -0
  50. package/src/components/Card/Card.definition.ts +5 -0
  51. package/src/components/Card/Card.stories.tsx +221 -0
  52. package/src/components/Card/Card.sx.ts +104 -0
  53. package/src/components/Card/Card.tsx +200 -0
  54. package/src/components/Card/index.ts +9 -0
  55. package/src/components/Chip/Chip.definitions.ts +167 -0
  56. package/src/components/Chip/Chip.stories.tsx +265 -0
  57. package/src/components/Chip/Chip.tsx +61 -0
  58. package/src/components/Chip/index.ts +1 -0
  59. package/src/components/Column/Column.tsx +29 -0
  60. package/src/components/Column/index.ts +1 -0
  61. package/src/components/DatePicker/DatePicker.definitions.ts +228 -0
  62. package/src/components/DatePicker/DatePicker.helpers.ts +24 -0
  63. package/src/components/DatePicker/DatePicker.stories.tsx +309 -0
  64. package/src/components/DatePicker/DatePicker.sx.ts +33 -0
  65. package/src/components/DatePicker/DatePicker.tsx +189 -0
  66. package/src/components/DatePicker/DatePicker.types.ts +10 -0
  67. package/src/components/DatePicker/index.ts +9 -0
  68. package/src/components/DateRangePicker/DateRangePicker.definitions.ts +191 -0
  69. package/src/components/DateRangePicker/DateRangePicker.stories.tsx +252 -0
  70. package/src/components/DateRangePicker/DateRangePicker.tsx +56 -0
  71. package/src/components/DateRangePicker/index.ts +1 -0
  72. package/src/components/DateTimePicker/DateTimePicker.definitions.ts +256 -0
  73. package/src/components/DateTimePicker/DateTimePicker.helpers.ts +38 -0
  74. package/src/components/DateTimePicker/DateTimePicker.stories.tsx +418 -0
  75. package/src/components/DateTimePicker/DateTimePicker.sx.ts +30 -0
  76. package/src/components/DateTimePicker/DateTimePicker.tsx +225 -0
  77. package/src/components/DateTimePicker/DateTimePicker.types.ts +10 -0
  78. package/src/components/DateTimePicker/index.ts +9 -0
  79. package/src/components/Drawer/Drawer.stories.tsx +270 -0
  80. package/src/components/Drawer/Drawer.sx.ts +106 -0
  81. package/src/components/Drawer/Drawer.tsx +214 -0
  82. package/src/components/Drawer/DrawerContext.ts +26 -0
  83. package/src/components/Drawer/DrawerItem.tsx +110 -0
  84. package/src/components/Drawer/index.ts +10 -0
  85. package/src/components/Flyout/Flyout.stories.tsx +282 -0
  86. package/src/components/Flyout/Flyout.tsx +122 -0
  87. package/src/components/Flyout/index.ts +1 -0
  88. package/src/components/Gallery/Gallery.definition.tsx +37 -0
  89. package/src/components/Gallery/Gallery.stories.tsx +82 -0
  90. package/src/components/Gallery/Gallery.tsx +118 -0
  91. package/src/components/Gallery/GalleryLightbox.tsx +170 -0
  92. package/src/components/Gallery/GalleryMain.tsx +84 -0
  93. package/src/components/Gallery/GalleryThumbnails.tsx +106 -0
  94. package/src/components/Gallery/index.ts +1 -0
  95. package/src/components/Icon/Icon.stories.tsx +121 -0
  96. package/src/components/Icon/Icon.tsx +175 -0
  97. package/src/components/Icon/index.ts +2 -0
  98. package/src/components/Input/Input.definitions.ts +324 -0
  99. package/src/components/Input/Input.helpers.ts +49 -0
  100. package/src/components/Input/Input.stories.tsx +499 -0
  101. package/src/components/Input/Input.sx.ts +42 -0
  102. package/src/components/Input/Input.tsx +141 -0
  103. package/src/components/Input/Input.types.ts +10 -0
  104. package/src/components/Input/index.ts +9 -0
  105. package/src/components/InputGroup/InputGroup.definitions.ts +158 -0
  106. package/src/components/InputGroup/InputGroup.stories.tsx +267 -0
  107. package/src/components/InputGroup/InputGroup.tsx +179 -0
  108. package/src/components/InputGroup/index.ts +1 -0
  109. package/src/components/MenuButton/MenuButton.stories.tsx +197 -0
  110. package/src/components/MenuButton/MenuButton.tsx +100 -0
  111. package/src/components/MenuButton/index.ts +1 -0
  112. package/src/components/Modal/Modal.stories.tsx +721 -0
  113. package/src/components/Modal/Modal.tsx +355 -0
  114. package/src/components/Modal/ModalBody.tsx +16 -0
  115. package/src/components/Modal/ModalFooter.tsx +71 -0
  116. package/src/components/Modal/ModalHeader.tsx +18 -0
  117. package/src/components/Modal/index.ts +6 -0
  118. package/src/components/PageLoader/PageLoader.stories.tsx +217 -0
  119. package/src/components/PageLoader/PageLoader.tsx +96 -0
  120. package/src/components/PageLoader/index.ts +2 -0
  121. package/src/components/ScrollTopButton/ScrollTopButton.stories.tsx +158 -0
  122. package/src/components/ScrollTopButton/ScrollTopButton.tsx +135 -0
  123. package/src/components/ScrollTopButton/index.ts +8 -0
  124. package/src/components/ScrollTopButton/scrollToTop.ts +37 -0
  125. package/src/components/Select/Select.definitions.ts +602 -0
  126. package/src/components/Select/Select.helpers.ts +71 -0
  127. package/src/components/Select/Select.stories.tsx +687 -0
  128. package/src/components/Select/Select.sx.ts +14 -0
  129. package/src/components/Select/Select.tsx +429 -0
  130. package/src/components/Select/Select.types.ts +15 -0
  131. package/src/components/Select/_parts/SelectMenuItem.tsx +40 -0
  132. package/src/components/Select/_parts/SelectSearchHeader.tsx +51 -0
  133. package/src/components/Select/_parts/SelectValue.tsx +96 -0
  134. package/src/components/Select/index.ts +14 -0
  135. package/src/components/Stat/Stat.stories.tsx +85 -0
  136. package/src/components/Stat/Stat.tsx +117 -0
  137. package/src/components/Stat/index.ts +2 -0
  138. package/src/components/StatusMessage/StatusMessage.stories.tsx +130 -0
  139. package/src/components/StatusMessage/StatusMessage.tsx +162 -0
  140. package/src/components/StatusMessage/index.ts +2 -0
  141. package/src/components/Stepper/Step.tsx +21 -0
  142. package/src/components/Stepper/Stepper.definition.ts +75 -0
  143. package/src/components/Stepper/Stepper.stories.tsx +122 -0
  144. package/src/components/Stepper/Stepper.tsx +75 -0
  145. package/src/components/Stepper/index.ts +2 -0
  146. package/src/components/Table/EmptyTable.png +0 -0
  147. package/src/components/Table/Table.definition.ts +580 -0
  148. package/src/components/Table/Table.stories.tsx +853 -0
  149. package/src/components/Table/Table.tsx +495 -0
  150. package/src/components/Table/data.ts +134 -0
  151. package/src/components/Table/exportsUtils.ts +195 -0
  152. package/src/components/Table/index.ts +3 -0
  153. package/src/components/Table/types.ts +34 -0
  154. package/src/components/Tabs/Tab.definition.ts +53 -0
  155. package/src/components/Tabs/Tab.tsx +19 -0
  156. package/src/components/Tabs/Tabs.stories.tsx +118 -0
  157. package/src/components/Tabs/Tabs.tsx +99 -0
  158. package/src/components/Tabs/_tabUtils.tsx +4 -0
  159. package/src/components/Tabs/index.ts +2 -0
  160. package/src/components/Timeline/Timeline.definition.ts +43 -0
  161. package/src/components/Timeline/Timeline.stories.tsx +108 -0
  162. package/src/components/Timeline/Timeline.tsx +49 -0
  163. package/src/components/Timeline/TimelineItem.tsx +31 -0
  164. package/src/components/Timeline/index.ts +2 -0
  165. package/src/components/Tooltip/Tooltip.stories.tsx +129 -0
  166. package/src/components/Tooltip/Tooltip.tsx +58 -0
  167. package/src/components/Tooltip/index.ts +1 -0
  168. package/src/components/_shared/formField.sx.ts +118 -0
  169. package/src/components/_shared/resolvePreset.ts +35 -0
  170. package/src/hooks/ClipBoard/ClipBoard.stories.tsx +168 -0
  171. package/src/hooks/ClipBoard/ClipBoard.tsx +131 -0
  172. package/src/hooks/ClipBoard/ClipboardUnifiedDemo.tsx +111 -0
  173. package/src/hooks/ClipBoard/index.ts +1 -0
  174. package/src/hooks/Wizard/Wizard.stories.tsx +301 -0
  175. package/src/hooks/Wizard/WizardContext.tsx +166 -0
  176. package/src/hooks/Wizard/index.ts +6 -0
  177. package/src/hooks/Wizard/useWizard.ts +13 -0
  178. package/src/index.ts +17 -0
  179. package/src/mui.ts +54 -0
  180. package/src/styles.css +3 -0
  181. package/src/theme/componentStyles.ts +47 -0
  182. package/src/theme/tokens.ts +43 -0
  183. package/tailwind.config.js +10 -0
  184. package/tsconfig.json +48 -0
  185. package/tsup.config.js +41 -0
  186. package/vite.config.js +132 -0
  187. package/vitest.config.ts +35 -0
@@ -0,0 +1,258 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { within, expect } from 'storybook/test';
3
+ import { Avatar } from './Avatar';
4
+ import {
5
+ DefaultAvatarDefinition,
6
+ WithTypeAndTextDefinition,
7
+ WithBadgesAndTooltipDefinition,
8
+ SizeSmallDefinition,
9
+ SizeMediumDefinition,
10
+ SizeLargeDefinition,
11
+ SizeExtraLargeDefinition,
12
+ NumericSizeDefinition,
13
+ FallbackIconDefinition,
14
+ DisplayedAvatarsLimitDefinition,
15
+ CustomStylingDefinition,
16
+ } from './Avatar.definitions';
17
+
18
+ const meta: Meta<typeof Avatar> = {
19
+ title: 'Components/Avatar',
20
+ component: Avatar,
21
+ parameters: {
22
+ layout: 'centered',
23
+ docs: {
24
+ description: {
25
+ component:
26
+ 'Avatar basado en MUI Avatar con soporte para stacking, tooltips, fallback a icono/badge y passthrough de sx. Admite imagen, texto y badge por cada item.',
27
+ },
28
+ },
29
+ },
30
+ tags: ['autodocs'],
31
+ argTypes: {
32
+ items: {
33
+ control: 'object',
34
+ description: 'Array de `AvatarItem` (text/imageUrl/badge/color/backgroundColor).',
35
+ },
36
+ type: {
37
+ control: 'text',
38
+ description: 'Etiqueta que antecede al texto cuando hay un único item (ej. "Owner: Maria").',
39
+ },
40
+ displayedAvatars: {
41
+ control: 'number',
42
+ description: 'Número máximo de avatares visibles. El resto se contabiliza como `+N`.',
43
+ },
44
+ size: {
45
+ control: 'select',
46
+ options: ['sm', 'md', 'lg', 'xl'],
47
+ description: 'Tamaño del avatar. Acepta presets (`sm | md | lg | xl`) o un número libre en px.',
48
+ },
49
+ showText: {
50
+ control: 'boolean',
51
+ description: 'Muestra el texto al lado del avatar.',
52
+ },
53
+ showTooltip: {
54
+ control: 'boolean',
55
+ description: 'Muestra el tooltip con `item.text` al hover.',
56
+ },
57
+ overlap: {
58
+ control: 'number',
59
+ description: 'Overlap en px entre avatares cuando hay varios (override del default por tamaño).',
60
+ },
61
+ sx: {
62
+ control: false,
63
+ description: 'sx del contenedor raíz. Se mergea sobre los defaults.',
64
+ },
65
+ avatarSx: {
66
+ control: false,
67
+ description: 'sx aplicado a cada MuiAvatar individual (borde, colores, etc).',
68
+ },
69
+ },
70
+ };
71
+
72
+ export default meta;
73
+ type Story = StoryObj<typeof Avatar>;
74
+
75
+ export const Default: Story = {
76
+ args: {
77
+ items: [
78
+ { text: 'User One', imageUrl: 'https://i.pravatar.cc/150?img=1' },
79
+ { text: 'User Two', imageUrl: 'https://i.pravatar.cc/150?img=2' },
80
+ ],
81
+ },
82
+ parameters: {
83
+ docs: { source: { code: DefaultAvatarDefinition.trim() } },
84
+ },
85
+ };
86
+
87
+ export const WithTypeAndText: Story = {
88
+ args: {
89
+ type: 'Owner',
90
+ items: [{ text: 'Maria', imageUrl: 'https://i.pravatar.cc/150?img=5' }],
91
+ showText: true,
92
+ },
93
+ play: async ({ canvasElement }) => {
94
+ const canvas = within(canvasElement);
95
+ const text = await canvas.findByTestId('text');
96
+ const type = await canvas.findByTestId('type');
97
+ expect(text).toBeInTheDocument();
98
+ expect(type).toHaveTextContent('Owner:');
99
+ },
100
+ parameters: {
101
+ docs: { source: { code: WithTypeAndTextDefinition.trim() } },
102
+ },
103
+ };
104
+
105
+ export const WithBadgesAndTooltip: Story = {
106
+ args: {
107
+ showTooltip: true,
108
+ items: [
109
+ { text: 'John D.', badge: 'JD', backgroundColor: '#EF5350', color: '#fff' },
110
+ { text: 'Alice B.', badge: 'AB', backgroundColor: '#AB47BC', color: '#fff' },
111
+ ],
112
+ },
113
+ play: async ({ canvasElement }) => {
114
+ const canvas = within(canvasElement);
115
+ const badges = await canvas.findAllByTestId('badge');
116
+ expect(badges.length).toBe(2);
117
+ },
118
+ parameters: {
119
+ docs: { source: { code: WithBadgesAndTooltipDefinition.trim() } },
120
+ },
121
+ };
122
+
123
+ export const SizeSmall: Story = {
124
+ args: {
125
+ size: 'sm',
126
+ items: [
127
+ { text: 'User One', imageUrl: 'https://i.pravatar.cc/150?img=11' },
128
+ { text: 'User Two', imageUrl: 'https://i.pravatar.cc/150?img=12' },
129
+ { text: 'User Three', imageUrl: 'https://i.pravatar.cc/150?img=13' },
130
+ ],
131
+ },
132
+ parameters: {
133
+ docs: { source: { code: SizeSmallDefinition.trim() } },
134
+ },
135
+ };
136
+
137
+ export const SizeMedium: Story = {
138
+ args: {
139
+ size: 'md',
140
+ items: [
141
+ { text: 'User One', imageUrl: 'https://i.pravatar.cc/150?img=21' },
142
+ { text: 'User Two', imageUrl: 'https://i.pravatar.cc/150?img=22' },
143
+ { text: 'User Three', imageUrl: 'https://i.pravatar.cc/150?img=23' },
144
+ ],
145
+ },
146
+ parameters: {
147
+ docs: { source: { code: SizeMediumDefinition.trim() } },
148
+ },
149
+ };
150
+
151
+ export const SizeLarge: Story = {
152
+ args: {
153
+ size: 'lg',
154
+ items: [
155
+ { text: 'User One', imageUrl: 'https://i.pravatar.cc/150?img=31' },
156
+ { text: 'User Two', imageUrl: 'https://i.pravatar.cc/150?img=32' },
157
+ ],
158
+ },
159
+ parameters: {
160
+ docs: { source: { code: SizeLargeDefinition.trim() } },
161
+ },
162
+ };
163
+
164
+ export const SizeExtraLarge: Story = {
165
+ args: {
166
+ size: 'xl',
167
+ items: [{ text: 'Maria', imageUrl: 'https://i.pravatar.cc/150?img=5' }],
168
+ showText: false,
169
+ },
170
+ parameters: {
171
+ docs: { source: { code: SizeExtraLargeDefinition.trim() } },
172
+ },
173
+ };
174
+
175
+ export const NumericSize: Story = {
176
+ args: {
177
+ size: 72,
178
+ items: [
179
+ { text: 'User One', imageUrl: 'https://i.pravatar.cc/150?img=41' },
180
+ { text: 'User Two', imageUrl: 'https://i.pravatar.cc/150?img=42' },
181
+ ],
182
+ showText: false,
183
+ },
184
+ parameters: {
185
+ docs: {
186
+ description: {
187
+ story: 'Pasando un número en `size` se calcula automáticamente el borde, font-size y overlap proporcional.',
188
+ },
189
+ source: { code: NumericSizeDefinition.trim() },
190
+ },
191
+ },
192
+ };
193
+
194
+ export const FallbackIcon: Story = {
195
+ args: {
196
+ showTooltip: true,
197
+ items: [
198
+ { text: 'Without image' },
199
+ { text: 'Broken image', imageUrl: 'https://example.invalid/broken.png' },
200
+ ],
201
+ },
202
+ parameters: {
203
+ docs: {
204
+ description: {
205
+ story: 'Cuando no hay imagen (o falla la carga) y no hay badge, cae al icono `AccountCircle`.',
206
+ },
207
+ source: { code: FallbackIconDefinition.trim() },
208
+ },
209
+ },
210
+ };
211
+
212
+ export const DisplayedAvatarsLimit: Story = {
213
+ args: {
214
+ displayedAvatars: 3,
215
+ items: [
216
+ { text: 'User 1', imageUrl: 'https://i.pravatar.cc/150?img=51' },
217
+ { text: 'User 2', imageUrl: 'https://i.pravatar.cc/150?img=52' },
218
+ { text: 'User 3', imageUrl: 'https://i.pravatar.cc/150?img=53' },
219
+ { text: 'User 4', imageUrl: 'https://i.pravatar.cc/150?img=54' },
220
+ { text: 'User 5', imageUrl: 'https://i.pravatar.cc/150?img=55' },
221
+ ],
222
+ },
223
+ parameters: {
224
+ docs: {
225
+ description: {
226
+ story: 'Solo se renderizan `displayedAvatars` avatares; el resto se cuenta en el `+N` del texto.',
227
+ },
228
+ source: { code: DisplayedAvatarsLimitDefinition.trim() },
229
+ },
230
+ },
231
+ };
232
+
233
+ export const CustomStyling: Story = {
234
+ args: {
235
+ size: 'md',
236
+ items: [
237
+ { text: 'User One', imageUrl: 'https://i.pravatar.cc/150?img=61' },
238
+ { text: 'User Two', imageUrl: 'https://i.pravatar.cc/150?img=62' },
239
+ ],
240
+ sx: {
241
+ p: 1,
242
+ borderRadius: 2,
243
+ bgcolor: 'action.hover',
244
+ },
245
+ avatarSx: {
246
+ border: (theme) => `3px solid ${theme.palette.primary.main}`,
247
+ },
248
+ },
249
+ parameters: {
250
+ docs: {
251
+ description: {
252
+ story:
253
+ 'Demo del passthrough de `sx` (contenedor) y `avatarSx` (cada avatar). El consumidor puede overridear bordes, colores y el layout del wrapper sin tocar el componente.',
254
+ },
255
+ source: { code: CustomStylingDefinition.trim() },
256
+ },
257
+ },
258
+ };
@@ -0,0 +1,206 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ Avatar as MuiAvatar,
4
+ Box,
5
+ Tooltip,
6
+ Typography,
7
+ type SxProps,
8
+ type Theme,
9
+ } from '@mui/material';
10
+ import AccountCircleIcon from '@mui/icons-material/AccountCircle';
11
+
12
+ export interface AvatarItem {
13
+ text?: string;
14
+ imageUrl?: string;
15
+ badge?: string;
16
+ color?: string;
17
+ backgroundColor?: string;
18
+ }
19
+
20
+ export type AvatarSize = 'sm' | 'md' | 'lg' | 'xl' | number;
21
+
22
+ export interface AvatarProps {
23
+ type?: string;
24
+ items: AvatarItem[];
25
+ displayedAvatars?: number;
26
+ size?: AvatarSize;
27
+ showText?: boolean;
28
+ showTooltip?: boolean;
29
+ /**
30
+ * sx aplicado al contenedor raíz.
31
+ */
32
+ sx?: SxProps<Theme>;
33
+ /**
34
+ * sx aplicado a cada MuiAvatar individual (se mergea sobre los defaults).
35
+ */
36
+ avatarSx?: SxProps<Theme>;
37
+ className?: string;
38
+ /**
39
+ * Overlap (px) entre avatares cuando hay varios. Default depende del tamaño.
40
+ */
41
+ overlap?: number;
42
+ }
43
+
44
+ // Escala alineada con la escala de MUI (sm=32, md=40, lg=56, xl=96) con borde
45
+ // proporcional para el efecto stacked.
46
+ const sizeMap: Record<
47
+ Exclude<AvatarSize, number>,
48
+ { px: number; border: number; font: number; overlap: number }
49
+ > = {
50
+ sm: { px: 32, border: 2, font: 14, overlap: 8 },
51
+ md: { px: 40, border: 2, font: 16, overlap: 10 },
52
+ lg: { px: 56, border: 3, font: 22, overlap: 14 },
53
+ xl: { px: 96, border: 4, font: 36, overlap: 20 },
54
+ };
55
+
56
+ const resolveSize = (size: AvatarSize) => {
57
+ if (typeof size === 'number') {
58
+ return {
59
+ px: size,
60
+ border: Math.max(2, Math.round(size * 0.05)),
61
+ font: Math.round(size * 0.4),
62
+ overlap: Math.round(size * 0.25),
63
+ };
64
+ }
65
+ return sizeMap[size];
66
+ };
67
+
68
+ const mergeSx = (base: SxProps<Theme>, extra?: SxProps<Theme>): SxProps<Theme> => {
69
+ if (!extra) return base;
70
+ const baseArr = Array.isArray(base) ? base : [base];
71
+ const extraArr = Array.isArray(extra) ? extra : [extra];
72
+ return [...baseArr, ...extraArr] as SxProps<Theme>;
73
+ };
74
+
75
+ export const Avatar: React.FC<AvatarProps> = ({
76
+ items,
77
+ type,
78
+ displayedAvatars = 4,
79
+ size = 'sm',
80
+ showText = true,
81
+ showTooltip = false,
82
+ sx,
83
+ avatarSx,
84
+ className,
85
+ overlap,
86
+ }) => {
87
+ const [errorIndex, setErrorIndex] = useState<Set<number>>(new Set());
88
+
89
+ const handleImageError = (index: number) => {
90
+ setErrorIndex((prev) => {
91
+ const next = new Set(prev);
92
+ next.add(index);
93
+ return next;
94
+ });
95
+ };
96
+
97
+ if (!items || items.length === 0) {
98
+ return null;
99
+ }
100
+
101
+ const s = resolveSize(size);
102
+ const effectiveOverlap = overlap ?? s.overlap;
103
+ const visibleItems = items.slice(0, displayedAvatars);
104
+
105
+ const baseAvatarSx: SxProps<Theme> = {
106
+ width: s.px,
107
+ height: s.px,
108
+ fontSize: s.font,
109
+ fontWeight: 700,
110
+ border: (theme) => `${s.border}px solid ${theme.palette.background.paper}`,
111
+ boxSizing: 'content-box',
112
+ };
113
+
114
+ const renderSingleAvatar = (item: AvatarItem, i: number) => {
115
+ const hasImage = !!item.imageUrl && !errorIndex.has(i);
116
+ const showBadgeFallback = !!item.badge;
117
+
118
+ // Defaults tirando al theme; item.color / item.backgroundColor tienen prioridad.
119
+ const itemSx: SxProps<Theme> = {
120
+ bgcolor: item.backgroundColor ?? 'action.selected',
121
+ color: item.color ?? 'text.secondary',
122
+ // Stacking manual: margen negativo al segundo avatar en adelante.
123
+ ...(i > 0 && { marginLeft: `-${effectiveOverlap}px` }),
124
+ zIndex: visibleItems.length - i,
125
+ };
126
+
127
+ const finalSx = mergeSx(mergeSx(baseAvatarSx, itemSx), avatarSx);
128
+
129
+ const avatarEl = (
130
+ <MuiAvatar
131
+ data-testid="avatar"
132
+ alt={item.text || 'User avatar'}
133
+ src={hasImage ? item.imageUrl : undefined}
134
+ imgProps={{
135
+ 'data-testid': 'image',
136
+ onError: () => handleImageError(i),
137
+ } as React.ImgHTMLAttributes<HTMLImageElement>}
138
+ sx={finalSx}
139
+ >
140
+ {!hasImage && showBadgeFallback ? (
141
+ <span data-testid="badge" aria-label={item.text}>
142
+ {item.badge}
143
+ </span>
144
+ ) : !hasImage ? (
145
+ <AccountCircleIcon
146
+ data-testid="icon"
147
+ aria-label={item.text}
148
+ sx={{ width: '100%', height: '100%' }}
149
+ />
150
+ ) : null}
151
+ </MuiAvatar>
152
+ );
153
+
154
+ if (showTooltip && item.text) {
155
+ return (
156
+ <Tooltip key={i} title={item.text}>
157
+ {avatarEl}
158
+ </Tooltip>
159
+ );
160
+ }
161
+ return <React.Fragment key={i}>{avatarEl}</React.Fragment>;
162
+ };
163
+
164
+ return (
165
+ <Box
166
+ data-testid="avatar-container"
167
+ className={className}
168
+ sx={mergeSx(
169
+ {
170
+ display: 'flex',
171
+ alignItems: 'center',
172
+ lineHeight: 1,
173
+ width: 'fit-content',
174
+ },
175
+ sx,
176
+ )}
177
+ >
178
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
179
+ {visibleItems.map((item, i) => renderSingleAvatar(item, i))}
180
+ </Box>
181
+
182
+ {showText && items[0]?.text && (
183
+ <Typography
184
+ data-testid="text"
185
+ variant="caption"
186
+ sx={{
187
+ ml: 1,
188
+ fontSize: '0.75rem',
189
+ fontWeight: 400,
190
+ color: 'text.primary',
191
+ }}
192
+ >
193
+ {type && items.length === 1 && (
194
+ <Box component="span" data-testid="type" sx={{ mr: 0.5 }}>
195
+ {type}:
196
+ </Box>
197
+ )}
198
+ {items[0].text}
199
+ {items.length > 1 && ` +${items.length - 1}`}
200
+ </Typography>
201
+ )}
202
+ </Box>
203
+ );
204
+ };
205
+
206
+ export default Avatar;
@@ -0,0 +1 @@
1
+ export { default as Avatar} from './Avatar'
@@ -0,0 +1,97 @@
1
+ export const DefaultButtonDefinition = `
2
+ <Button variant="text">
3
+ Button
4
+ </Button>
5
+ `
6
+
7
+ export const OutlinedButtonDefinition = `
8
+ <Button variant="outlined">
9
+ Button
10
+ </Button>
11
+ `
12
+
13
+ export const ContainedButtonDefinition = `
14
+ <Button variant="contained">
15
+ Button
16
+ </Button>
17
+ `
18
+
19
+ export const WithStartIconButtonDefinition = `
20
+ <Button
21
+ data-testid="button"
22
+ startIcon={<NotificationsIcon/>}
23
+ variant="contained"
24
+ >
25
+ Button
26
+ </Button>
27
+ `
28
+
29
+ export const WithEndIconButtonDefinition = `
30
+ <Button
31
+ data-testid="button"
32
+ endIcon={<NotificationsIcon/>}
33
+ variant="contained"
34
+ >
35
+ Button
36
+ </Button>
37
+ `
38
+
39
+ export const WithSmallSizeButtonDefinition = `
40
+ <Button
41
+ data-testid="button"
42
+ size="small"
43
+ variant="contained"
44
+ >
45
+ Button
46
+ </Button>
47
+ `
48
+
49
+ export const WithLargeSizeButtonDefinition = `
50
+ <Button
51
+ data-testid="button"
52
+ size="large"
53
+ variant="contained"
54
+ >
55
+ Button
56
+ </Button>
57
+ `
58
+
59
+ export const DisabledStateButtonDefinition = `
60
+ <Button
61
+ data-testid="button"
62
+ disabled
63
+ variant="contained"
64
+ >
65
+ Button
66
+ </Button>`
67
+
68
+ export const LoadingButtonDefinition = `
69
+ <Button
70
+ data-testid="button"
71
+ loading
72
+ variant="contained"
73
+ >
74
+ Button
75
+ </Button>
76
+ `
77
+
78
+ export const LoadingIndicatorStartButtonDefinition = `
79
+ <Button
80
+ data-testid="button"
81
+ loading
82
+ loadingPosition="start"
83
+ variant="contained"
84
+ >
85
+ Button
86
+ </Button>
87
+ `
88
+
89
+ export const LoadingIndicatorEndButtonDefinition = `
90
+ <Button
91
+ data-testid="button"
92
+ loading
93
+ loadingPosition="end"
94
+ variant="contained"
95
+ >
96
+ Button
97
+ </Button>`