design-system-dashboard-devmunity 0.4.0 → 1.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.
Files changed (53) hide show
  1. package/README.md +33 -31
  2. package/app/app.config.ts +2 -1
  3. package/app/app.vue +21 -3
  4. package/app/assets/css/themes/components/input.js +5 -0
  5. package/app/assets/css/themes/index.js +1 -0
  6. package/app/components/BaseButton.vue +3 -0
  7. package/app/components/Colors.mdx +42 -0
  8. package/app/components/Indroduction.mdx +100 -0
  9. package/app/components/a/button/a-button-avatar-dropdown.stories.ts +83 -0
  10. package/app/components/a/button/a-button-avatar-dropdown.vue +41 -17
  11. package/app/components/a/button/a-button-navigation.stories.ts +66 -0
  12. package/app/components/a/button/a-button-navigation.vue +57 -0
  13. package/app/components/a/card/a-card-inner.stories.ts +89 -0
  14. package/app/components/a/card/a-card-inner.vue +26 -13
  15. package/app/components/a/dropdown/a-dropdown-avatar.stories.ts +160 -0
  16. package/app/components/a/dropdown/a-dropdown-avatar.vue +75 -33
  17. package/app/components/a/pill/a-pill.stories.ts +91 -0
  18. package/app/components/a/pill/a-pill.vue +63 -42
  19. package/app/components/b/badge/b-badge.stories.ts +77 -0
  20. package/app/components/b/badge/b-badge.vue +43 -19
  21. package/app/components/b/card/b-card.stories.ts +120 -0
  22. package/app/components/b/card/b-card.vue +49 -32
  23. package/app/components/b/modal/b-modal.stories.ts +210 -0
  24. package/app/components/b/modal/b-modal.vue +125 -81
  25. package/app/components/c/badge/c-badge-status.stories.ts +72 -0
  26. package/app/components/c/badge/c-badge-status.vue +36 -15
  27. package/app/components/c/modal/c-modal-danger.stories.ts +112 -0
  28. package/app/components/c/modal/c-modal-danger.vue +60 -41
  29. package/app/components/d/action-buttons/d-action-buttons.stories.ts +90 -0
  30. package/app/components/d/action-buttons/d-action-buttons.vue +121 -0
  31. package/app/components/d/card/d-card-header.stories.ts +123 -0
  32. package/app/components/d/card/d-card-header.vue +59 -41
  33. package/app/components/d/upload/d-upload-avatar.stories.ts +66 -0
  34. package/app/components/d/upload/d-upload-avatar.vue +49 -30
  35. package/app/types/index.ts +1 -0
  36. package/app/types/semantic-colors.type.ts +3 -0
  37. package/app/utils/util-get-colors-from-css.ts +53 -0
  38. package/nuxt.config.ts +11 -16
  39. package/package.json +84 -68
  40. package/.editorconfig +0 -13
  41. package/.github/workflows/release.yml +0 -36
  42. package/.husky/commit-msg +0 -1
  43. package/.husky/pre-commit +0 -0
  44. package/.prettierrc +0 -24
  45. package/.storybook/main.js +0 -25
  46. package/.storybook/preview.js +0 -13
  47. package/.vscode/settings.json +0 -28
  48. package/CHANGELOG.md +0 -55
  49. package/app/Introduction.mdx +0 -44
  50. package/app/components/a/button/a-button-back.vue +0 -33
  51. package/app/components/d/d-action-buttons.vue +0 -99
  52. package/commitlint.config.js +0 -8
  53. package/tsconfig.json +0 -8
@@ -0,0 +1,90 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { fn } from '@storybook/test'
3
+ import { semanticColors } from '@/utils/util-get-colors-from-css'
4
+ import DActionButtons from './d-action-buttons.vue'
5
+ import type { SemanticColors } from '@/types'
6
+
7
+ const meta = {
8
+ title: 'Design/ActionButtons',
9
+ component: DActionButtons,
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ 'Primary and secondary action buttons with layout variations. Generally used in sections that require a primary and/or secondary button, such as in the [DCardHeader](/docs/design-card-header--docs) section, or in the footer of modals like [BModal](/docs/bases-modal-bmodal--docs).',
15
+ },
16
+ },
17
+ },
18
+ argTypes: {
19
+ primaryButtonColor: {
20
+ control: 'select',
21
+ options: Object.keys(semanticColors) as SemanticColors[],
22
+ },
23
+ 'onOn-click-primary-button': {
24
+ table: { disable: true },
25
+ },
26
+ 'onOn-click-secondary-button': {
27
+ table: { disable: true },
28
+ },
29
+ },
30
+ args: {
31
+ 'onOn-click-primary-button': fn(),
32
+ 'onOn-click-secondary-button': fn(),
33
+ },
34
+ } satisfies Meta<typeof DActionButtons>
35
+
36
+ export default meta
37
+
38
+ type Story = StoryObj<typeof meta>
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ primaryButtonText: 'Confirm',
43
+ secondaryButtonText: 'Cancel',
44
+ },
45
+ }
46
+
47
+ export const WithIcon: Story = {
48
+ args: {
49
+ ...Default.args,
50
+ primaryButtonText: 'Add',
51
+ primaryButtonIcon: 'i-lucide-plus',
52
+ },
53
+ }
54
+
55
+ export const WithTrailingIcon: Story = {
56
+ args: {
57
+ primaryButtonText: 'Next',
58
+ primaryButtonTrailingIcon: 'i-lucide-arrow-right',
59
+ secondaryButtonText: 'Back',
60
+ },
61
+ }
62
+
63
+ export const Block: Story = {
64
+ args: {
65
+ ...Default.args,
66
+ hasButtonsBlock: true,
67
+ },
68
+ }
69
+
70
+ export const Reverse: Story = {
71
+ args: {
72
+ primaryButtonText: 'Next',
73
+ secondaryButtonText: 'Back',
74
+ isReverse: true,
75
+ },
76
+ }
77
+
78
+ export const DisabledPrimary: Story = {
79
+ args: {
80
+ ...Default.args,
81
+ isPrimaryButtonDisabled: true,
82
+ },
83
+ }
84
+
85
+ export const OnlyPrimaryAndUrl: Story = {
86
+ args: {
87
+ primaryButtonText: 'Go to home',
88
+ primaryButtonTo: '/',
89
+ },
90
+ }
@@ -0,0 +1,121 @@
1
+ <script lang="ts" setup>
2
+ import { twMerge, type ClassNameValue } from 'tailwind-merge'
3
+ import type { UButton } from '#components'
4
+
5
+ // Derived types from UButton component
6
+ type ButtonColor = InstanceType<typeof UButton>['$props']['color']
7
+
8
+ interface Props {
9
+ /**
10
+ * Text for the primary button
11
+ */
12
+ primaryButtonText?: string
13
+ /**
14
+ * Icon for the left side of primary button
15
+ */
16
+ primaryButtonIcon?: string
17
+ /**
18
+ * Icon for the right side of primary button
19
+ */
20
+ primaryButtonTrailingIcon?: string
21
+ /**
22
+ * Navigation target for the primary button
23
+ */
24
+ primaryButtonTo?: string
25
+ /**
26
+ * Color variant for the primary button
27
+ */
28
+ primaryButtonColor?: ButtonColor
29
+ /**
30
+ * Text for the secondary button
31
+ */
32
+ secondaryButtonText?: string
33
+ /**
34
+ * Navigation target for the secondary button
35
+ */
36
+ secondaryButtonTo?: string
37
+ /**
38
+ * Whether buttons should be displayed occupying all available space
39
+ */
40
+ hasButtonsBlock?: boolean
41
+ /**
42
+ * Whether to reverse the button order
43
+ */
44
+ isReverse?: boolean
45
+ /**
46
+ * Whether the primary button is disabled
47
+ */
48
+ isPrimaryButtonDisabled?: boolean
49
+ /**
50
+ * Whether the secondary button is disabled
51
+ */
52
+ isSecondaryButtonDisabled?: boolean
53
+ /**
54
+ * Additional CSS classes for the buttons
55
+ */
56
+ classButtons?: ClassNameValue
57
+ /**
58
+ * Additional CSS classes for the container
59
+ */
60
+ class?: ClassNameValue
61
+ }
62
+
63
+ const props = withDefaults(defineProps<Props>(), {
64
+ primaryButtonText: '',
65
+ primaryButtonIcon: '',
66
+ primaryButtonTrailingIcon: '',
67
+ primaryButtonTo: '',
68
+ primaryButtonColor: 'primary',
69
+ secondaryButtonText: '',
70
+ secondaryButtonTo: '',
71
+ hasButtonsBlock: false,
72
+ isReverse: false,
73
+ isPrimaryButtonDisabled: false,
74
+ isSecondaryButtonDisabled: false,
75
+ classButtons: '',
76
+ class: '',
77
+ })
78
+
79
+ const emit = defineEmits<{
80
+ /**
81
+ * Emitted when the primary button is clicked
82
+ */
83
+ (e: 'on-click-primary-button'): void
84
+ /**
85
+ * Emitted when the secondary button is clicked
86
+ */
87
+ (e: 'on-click-secondary-button'): void
88
+ }>()
89
+
90
+ const classCard = computed(() => twMerge('flex gap-x-4', props.class, props.isReverse && 'flex-row-reverse'))
91
+ const classButtonsComputed = computed(() =>
92
+ twMerge('justify-center w-[150px]', props.hasButtonsBlock && 'flex-auto', props.classButtons)
93
+ )
94
+ </script>
95
+
96
+ <template>
97
+ <footer :class="classCard">
98
+ <UButton
99
+ v-if="secondaryButtonText"
100
+ :to="secondaryButtonTo"
101
+ :label="secondaryButtonText"
102
+ :disabled="isSecondaryButtonDisabled"
103
+ :class="classButtonsComputed"
104
+ variant="outline"
105
+ color="neutral"
106
+ @click="$emit('on-click-secondary-button')"
107
+ />
108
+ <UButton
109
+ v-if="primaryButtonText"
110
+ :label="primaryButtonText"
111
+ :to="primaryButtonTo"
112
+ :color="primaryButtonColor"
113
+ :icon="primaryButtonIcon"
114
+ :trailing-icon="primaryButtonTrailingIcon"
115
+ :disabled="isPrimaryButtonDisabled"
116
+ :class="classButtonsComputed"
117
+ type="submit"
118
+ @click="$emit('on-click-primary-button')"
119
+ />
120
+ </footer>
121
+ </template>
@@ -0,0 +1,123 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { fn } from '@storybook/test'
3
+ import DCardHeader from './d-card-header.vue'
4
+ import DActionButtons from '@/components/d/action-buttons/d-action-buttons.vue'
5
+ import BCard from '@/components/b/card/b-card.vue'
6
+ import ACardInner from '@/components/a/card/a-card-inner.vue'
7
+
8
+ const meta = {
9
+ title: 'Design/Card/DCardHeader',
10
+ component: DCardHeader,
11
+ parameters: {
12
+ docs: {
13
+ description: {
14
+ component:
15
+ 'Componente de encabezado para una sección principal, en la que se puede mostrar un título con un botón de icono al lado (usualmente usado para ir hacia atrás), un subtítulo y un espacio para agregar contenido de acción (usualmente botones de acción [DActionButtons](/docs/design-actionbuttons--docs)). Usado por lo general como header en cards como [BCard](/docs/bases-card-bcard--docs).',
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ variant: {
21
+ control: 'select',
22
+ options: ['main', 'secondary'] as const,
23
+ },
24
+ 'onOn-click-left-button-icon': {
25
+ table: { disable: true },
26
+ },
27
+ },
28
+ args: {
29
+ 'onOn-click-left-button-icon': fn(),
30
+ },
31
+ } satisfies Meta<typeof DCardHeader>
32
+
33
+ export default meta
34
+
35
+ type Story = StoryObj<typeof meta>
36
+
37
+ const render = (args: any) => ({
38
+ components: { DCardHeader, DActionButtons },
39
+ setup() {
40
+ return { args }
41
+ },
42
+ template: `
43
+ <DCardHeader v-bind="args">
44
+ <template #actions>
45
+ <DActionButtons primary-button-text="Primary" secondary-button-text="Secondary" />
46
+ </template>
47
+ </DCardHeader>
48
+ `,
49
+ })
50
+
51
+ export const Main: Story = {
52
+ args: {
53
+ title: 'Main Header',
54
+ subtitle: 'Main subtitle description',
55
+ variant: 'main',
56
+ leftButtonIcon: 'heroicons:arrow-left-16-solid',
57
+ hasLeftButtonIcon: true,
58
+ },
59
+ render,
60
+ }
61
+
62
+ export const Secondary: Story = {
63
+ args: {
64
+ title: 'Secondary Header',
65
+ subtitle: 'Secondary subtitle description',
66
+ variant: 'secondary',
67
+ hasLeftButtonIcon: false,
68
+ },
69
+ render,
70
+ }
71
+
72
+ export const WithABCard: Story = {
73
+ name: 'With a BCard',
74
+ args: {
75
+ title: 'With a BCard',
76
+ subtitle: 'Subtitle',
77
+ variant: 'main',
78
+ leftButtonIcon: 'heroicons:arrow-left-16-solid',
79
+ hasLeftButtonIcon: true,
80
+ },
81
+ render: (args: any) => ({
82
+ components: { DCardHeader, BCard, ACardInner, DActionButtons },
83
+ setup() {
84
+ return { args }
85
+ },
86
+ template: `
87
+ <BCard>
88
+ <template #header>
89
+ <DCardHeader v-bind="args">
90
+ <template #actions>
91
+ <DActionButtons primary-button-text="Guardar" secondary-button-text="Cancelar" />
92
+ </template>
93
+ </DCardHeader>
94
+ </template>
95
+ <template #default>
96
+ <ACardInner>
97
+ <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Placeat odit eos sint doloribus maxime vitae aperiam nesciunt nisi quasi delectus ducimus harum officiis odio ipsa, eaque nobis autem ullam aut!</p>
98
+ </ACardInner>
99
+ </template>
100
+ <template #footer>
101
+ <ACardInner>
102
+ <p>Card footer</p>
103
+ </ACardInner>
104
+ </template>
105
+ </BCard>
106
+ `,
107
+ }),
108
+ }
109
+
110
+ export const Simple: Story = {
111
+ args: {
112
+ title: 'Simple Header',
113
+ },
114
+ render: (args: any) => ({
115
+ components: { DCardHeader },
116
+ setup() {
117
+ return { args }
118
+ },
119
+ template: `
120
+ <DCardHeader v-bind="args" />
121
+ `,
122
+ }),
123
+ }
@@ -1,45 +1,63 @@
1
- <script lang="jsx" setup>
1
+ <script lang="ts" setup>
2
2
  import { twMerge } from 'tailwind-merge'
3
3
 
4
- const props = defineProps({
5
- title: {
6
- type: String,
7
- default: '',
8
- required: false,
9
- },
10
- subtitle: {
11
- type: String,
12
- default: '',
13
- required: false,
14
- },
15
- variant: {
16
- type: String,
17
- default: 'main', // main, secondary
18
- required: false,
19
- },
20
- hasLeftButtonIcon: {
21
- type: Boolean,
22
- default: false,
23
- required: false,
24
- },
25
- leftButtonIcon: {
26
- type: String,
27
- default: 'heroicons:arrow-left-16-solid',
28
- required: false,
29
- },
30
- leftButtonIconTo: {
31
- type: String,
32
- default: '',
33
- required: false,
34
- },
35
- classTitle: {
36
- type: String,
37
- default: '',
38
- required: false,
39
- },
4
+ interface Props {
5
+ /**
6
+ * The main title text
7
+ */
8
+ title: string
9
+ /**
10
+ * The subtitle text displayed below the title
11
+ */
12
+ subtitle?: string
13
+ /**
14
+ * The visual variant of the header
15
+ */
16
+ variant?: 'main' | 'secondary'
17
+ /**
18
+ * The icon to use for the left button
19
+ */
20
+ leftButtonIcon?: string
21
+ /**
22
+ * The navigation target for the left button
23
+ */
24
+ leftButtonIconTo?: string
25
+ /**
26
+ * Whether to show a left button icon
27
+ */
28
+ hasLeftButtonIcon?: boolean
29
+ /**
30
+ * Additional CSS classes for the title element
31
+ */
32
+ classTitle?: string
33
+ }
34
+
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ subtitle: '',
37
+ variant: 'main',
38
+ leftButtonIcon: 'heroicons:arrow-left-16-solid',
39
+ leftButtonIconTo: '',
40
+ hasLeftButtonIcon: false,
41
+ classTitle: '',
40
42
  })
41
43
 
42
- const emit = defineEmits(['on-click-left-button-icon'])
44
+ const emit = defineEmits<{
45
+ /**
46
+ * Emitted when the left button icon is clicked
47
+ */
48
+ (e: 'on-click-left-button-icon'): void
49
+ }>()
50
+
51
+ defineSlots<{
52
+ /**
53
+ * Slot for action buttons or elements
54
+ */
55
+ actions(): any
56
+ /**
57
+ * Default slot content (not used in this component)
58
+ */
59
+ default?(): any
60
+ }>()
43
61
 
44
62
  const headingTag = computed(() => {
45
63
  const tags = {
@@ -58,15 +76,15 @@ function handleClickLeftButtonIcon() {
58
76
  <ACardInner
59
77
  :class="[
60
78
  'text-default flex items-center justify-between',
61
- { 'bg-muted border-y border-neutral-200': variant === 'secondary' },
79
+ variant === 'secondary' && 'bg-muted border-y border-neutral-200',
62
80
  ]"
63
81
  >
64
82
  <div class="space-y-1">
65
83
  <div class="flex items-center gap-x-3">
66
- <AButtonBack
84
+ <AButtonNavigation
67
85
  v-if="hasLeftButtonIcon"
68
86
  :icon="leftButtonIcon"
69
- :To="leftButtonIconTo"
87
+ :to="leftButtonIconTo"
70
88
  :is-back-action="leftButtonIconTo"
71
89
  @on-click="handleClickLeftButtonIcon"
72
90
  />
@@ -0,0 +1,66 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import type { SemanticColors } from '@/types'
3
+ import { fn } from '@storybook/test'
4
+ import { semanticColors } from '@/utils/util-get-colors-from-css'
5
+ import DUploadAvatar, { type ButtonSize, type ButtonColor } from './d-upload-avatar.vue'
6
+
7
+ const meta = {
8
+ title: 'Design/Upload/DUploadAvatar',
9
+ component: DUploadAvatar,
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ 'Avatar upload and display component. Contains the logic to upload an image as an avatar and display a previously uploaded image.',
15
+ },
16
+ },
17
+ },
18
+ argTypes: {
19
+ buttonSize: {
20
+ control: 'select',
21
+ options: ['xs', 'sm', 'md', 'lg', 'xl'] satisfies ButtonSize[],
22
+ },
23
+ buttonColor: {
24
+ control: 'select',
25
+ options: Object.keys(semanticColors) as SemanticColors[],
26
+ },
27
+ 'onOn-upload-image': {
28
+ table: { disable: true },
29
+ },
30
+ },
31
+ args: {
32
+ 'onOn-upload-image': fn(),
33
+ },
34
+ } satisfies Meta<typeof DUploadAvatar>
35
+
36
+ export default meta
37
+
38
+ type Story = StoryObj<typeof meta>
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ buttonSize: 'sm',
43
+ },
44
+ }
45
+
46
+ export const WithImage: Story = {
47
+ args: {
48
+ ...Default.args,
49
+ src: 'https://avatars.githubusercontent.com/u/739984?v=4',
50
+ },
51
+ }
52
+
53
+ export const ButtonColorSuccess: Story = {
54
+ args: {
55
+ ...Default.args,
56
+ src: 'https://avatars.githubusercontent.com/u/739984?v=4',
57
+ buttonColor: 'success',
58
+ },
59
+ }
60
+
61
+ export const ButtonSizeLarge: Story = {
62
+ args: {
63
+ ...Default.args,
64
+ buttonSize: 'lg',
65
+ },
66
+ }
@@ -1,28 +1,44 @@
1
- <script setup>
2
- const props = defineProps({
3
- src: {
4
- type: String,
5
- default: '',
6
- required: false,
7
- },
8
- buttonSize: {
9
- type: String,
10
- default: 'sm',
11
- required: false,
12
- validator: (value) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(value),
13
- },
14
- buttonColor: {
15
- type: String,
16
- default: 'neutral',
17
- required: false,
18
- },
1
+ <script lang="ts" setup>
2
+ import type { UButton } from '#components'
3
+
4
+ // Types
5
+ export type ButtonSize = InstanceType<typeof UButton>['$props']['size']
6
+ export type ButtonColor = InstanceType<typeof UButton>['$props']['color']
7
+
8
+ interface Props {
9
+ /**
10
+ * The source URL of the previously uploaded avatar image before uploading a new one
11
+ */
12
+ src?: string
13
+ /**
14
+ * The size of the upload button
15
+ */
16
+ buttonSize?: ButtonSize
17
+ /**
18
+ * The color of the upload button
19
+ */
20
+ buttonColor?: ButtonColor
21
+ }
22
+
23
+ interface Emits {
24
+ /**
25
+ * Emitted when an image is uploaded
26
+ */
27
+ (e: 'on-upload-image', file: File): void
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ src: '',
32
+ buttonSize: 'sm',
33
+ buttonColor: 'neutral',
19
34
  })
20
- const emit = defineEmits('on-upload-image')
35
+
36
+ const emit = defineEmits<Emits>()
21
37
 
22
38
  // Data
23
39
 
24
- const fileName = ref(null)
25
- const inputFileRef = ref(null)
40
+ const fileName = ref<File | null>(null)
41
+ const inputFileRef = ref<HTMLInputElement | null>(null)
26
42
 
27
43
  // Computed
28
44
 
@@ -39,14 +55,17 @@ const getImage = computed(() => {
39
55
 
40
56
  // Methods
41
57
 
42
- function handleUploadImage(event) {
43
- if (event.target.files) {
44
- fileName.value = event.target.files[0]
45
- } else {
46
- fileName.value = event.dataTransfer.files[0]
47
- }
58
+ // Proposed change in handleUploadImage function
59
+ function handleUploadImage(event: Event): void {
60
+ const target = event.target as HTMLInputElement
61
+ const files = target.files || (event as DragEvent).dataTransfer?.files
48
62
 
49
- emit('on-upload-image', fileName.value)
63
+ // Capture the first file and check for its existence
64
+ const file = files?.[0]
65
+ if (file) {
66
+ fileName.value = file
67
+ emit('on-upload-image', file)
68
+ }
50
69
  }
51
70
  </script>
52
71
 
@@ -64,12 +83,12 @@ function handleUploadImage(event) {
64
83
  <div
65
84
  v-else
66
85
  class="bg-primary-50 grid size-20 place-items-center rounded-md"
67
- @click="inputFileRef.click()"
86
+ @click="inputFileRef?.click()"
68
87
  >
69
88
  <UIcon name="heroicons:photo" class="size-6 text-neutral-900" />
70
89
  </div>
71
90
  </div>
72
- <UButton :color="buttonColor" variant="outline" @click="inputFileRef.click()" :size="buttonSize">
91
+ <UButton :color="buttonColor" variant="outline" @click="inputFileRef?.click()" :size="buttonSize">
73
92
  {{ 'Cambiar' }}
74
93
  </UButton>
75
94
  </div>
@@ -0,0 +1 @@
1
+ export * from './semantic-colors.type'
@@ -0,0 +1,3 @@
1
+ import appConfig from '@/app.config'
2
+
3
+ export type SemanticColors = keyof typeof appConfig.ui.colors
@@ -0,0 +1,53 @@
1
+ import appConfig from '@/app.config'
2
+
3
+ export const semanticColors = appConfig.ui.colors
4
+
5
+ export type ColorMap = Record<string, string>
6
+ export type GroupedColors = Record<string, ColorMap>
7
+
8
+ export function getColorsFromCss(): GroupedColors {
9
+ if (typeof window === 'undefined') return {}
10
+
11
+ const styles = getComputedStyle(document.documentElement)
12
+ const colors: ColorMap = {}
13
+ for (let i = 0; i < styles.length; i++) {
14
+ const name = styles[i]
15
+ if (name && name.startsWith('--color-brand')) {
16
+ const key = name.replace('--color-', '')
17
+ colors[key] = styles.getPropertyValue(name).trim()
18
+ }
19
+ }
20
+
21
+ return groupColorsByName(colors)
22
+ }
23
+
24
+ function groupColorsByName(colors: ColorMap): GroupedColors {
25
+ // 1. Convertimos el objeto en entradas y extraemos nombre + nivel
26
+ const parsed = Object.entries(colors)
27
+ .map(([key, value]) => {
28
+ const match = key.match(/^([a-z-]+)-(\d+)$/)
29
+ if (!match) return null
30
+ const [, name, level] = match
31
+ return { name, level, value }
32
+ })
33
+ .filter((item): item is { name: string; level: string; value: string } => item !== null)
34
+
35
+ // 2. Agrupamos por nombre
36
+ const grouped = parsed.reduce<Record<string, typeof parsed>>((acc, item) => {
37
+ if (!acc[item.name]) {
38
+ acc[item.name] = []
39
+ }
40
+ acc[item.name]!.push(item)
41
+ return acc
42
+ }, {})
43
+
44
+ // 3. Construimos el objeto final
45
+ return Object.fromEntries(
46
+ Object.entries(grouped).map(([name, items]) => [
47
+ name,
48
+ Object.fromEntries(items.map(({ level, value }) => [level, value])),
49
+ ])
50
+ )
51
+ }
52
+
53
+ export const colorsFromCss = getColorsFromCss()