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.
- package/README.md +33 -31
- package/app/app.config.ts +2 -1
- package/app/app.vue +21 -3
- package/app/assets/css/themes/components/input.js +5 -0
- package/app/assets/css/themes/index.js +1 -0
- package/app/components/BaseButton.vue +3 -0
- package/app/components/Colors.mdx +42 -0
- package/app/components/Indroduction.mdx +100 -0
- package/app/components/a/button/a-button-avatar-dropdown.stories.ts +83 -0
- package/app/components/a/button/a-button-avatar-dropdown.vue +41 -17
- package/app/components/a/button/a-button-navigation.stories.ts +66 -0
- package/app/components/a/button/a-button-navigation.vue +57 -0
- package/app/components/a/card/a-card-inner.stories.ts +89 -0
- package/app/components/a/card/a-card-inner.vue +26 -13
- package/app/components/a/dropdown/a-dropdown-avatar.stories.ts +160 -0
- package/app/components/a/dropdown/a-dropdown-avatar.vue +75 -33
- package/app/components/a/pill/a-pill.stories.ts +91 -0
- package/app/components/a/pill/a-pill.vue +63 -42
- package/app/components/b/badge/b-badge.stories.ts +77 -0
- package/app/components/b/badge/b-badge.vue +43 -19
- package/app/components/b/card/b-card.stories.ts +120 -0
- package/app/components/b/card/b-card.vue +49 -32
- package/app/components/b/modal/b-modal.stories.ts +210 -0
- package/app/components/b/modal/b-modal.vue +125 -81
- package/app/components/c/badge/c-badge-status.stories.ts +72 -0
- package/app/components/c/badge/c-badge-status.vue +36 -15
- package/app/components/c/modal/c-modal-danger.stories.ts +112 -0
- package/app/components/c/modal/c-modal-danger.vue +60 -41
- package/app/components/d/action-buttons/d-action-buttons.stories.ts +90 -0
- package/app/components/d/action-buttons/d-action-buttons.vue +121 -0
- package/app/components/d/card/d-card-header.stories.ts +123 -0
- package/app/components/d/card/d-card-header.vue +59 -41
- package/app/components/d/upload/d-upload-avatar.stories.ts +66 -0
- package/app/components/d/upload/d-upload-avatar.vue +49 -30
- package/app/types/index.ts +1 -0
- package/app/types/semantic-colors.type.ts +3 -0
- package/app/utils/util-get-colors-from-css.ts +53 -0
- package/nuxt.config.ts +11 -16
- package/package.json +84 -68
- package/.editorconfig +0 -13
- package/.github/workflows/release.yml +0 -36
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -0
- package/.prettierrc +0 -24
- package/.storybook/main.js +0 -25
- package/.storybook/preview.js +0 -13
- package/.vscode/settings.json +0 -28
- package/CHANGELOG.md +0 -55
- package/app/Introduction.mdx +0 -44
- package/app/components/a/button/a-button-back.vue +0 -33
- package/app/components/d/d-action-buttons.vue +0 -99
- package/commitlint.config.js +0 -8
- 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="
|
|
1
|
+
<script lang="ts" setup>
|
|
2
2
|
import { twMerge } from 'tailwind-merge'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
84
|
+
<AButtonNavigation
|
|
67
85
|
v-if="hasLeftButtonIcon"
|
|
68
86
|
:icon="leftButtonIcon"
|
|
69
|
-
:
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,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()
|