Compare commits
2 Commits
b08d347727
...
0b1ab9261e
| Author | SHA1 | Date | |
|---|---|---|---|
|
0b1ab9261e
|
|||
|
79a8720fb0
|
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: 'success',
|
||||
validator: v => ['primary','secondary','success','warning','danger','error','info','light','dark','white'].includes(v),
|
||||
},
|
||||
dismissible: { type: Boolean, default: false },
|
||||
fontWeight: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['dismissed'])
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
function dismiss() {
|
||||
visible.value = false
|
||||
emit('dismissed')
|
||||
}
|
||||
|
||||
const colorClasses = {
|
||||
primary: 'bg-primary text-white',
|
||||
secondary: 'bg-secondary text-white',
|
||||
success: 'bg-success text-white',
|
||||
warning: 'bg-warning text-white',
|
||||
danger: 'bg-danger text-white',
|
||||
error: 'bg-danger text-white',
|
||||
info: 'bg-info text-white',
|
||||
light: 'bg-light text-dark',
|
||||
dark: 'bg-dark text-white',
|
||||
white: 'bg-white text-dark border border-gray-200',
|
||||
}
|
||||
|
||||
const weightClasses = {
|
||||
light: 'font-light',
|
||||
normal: 'font-normal',
|
||||
bold: 'font-bold',
|
||||
bolder: 'font-black',
|
||||
}
|
||||
|
||||
const classes = computed(() => [
|
||||
'relative flex items-start gap-3 rounded-lg px-4 py-3 text-sm',
|
||||
colorClasses[props.color] ?? colorClasses.success,
|
||||
props.fontWeight ? weightClasses[props.fontWeight] : '',
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="alert-fade">
|
||||
<div v-if="visible" :class="classes" role="alert">
|
||||
<slot />
|
||||
<button
|
||||
v-if="dismissible"
|
||||
type="button"
|
||||
class="ml-auto shrink-0 opacity-70 transition-opacity hover:opacity-100"
|
||||
aria-label="Close"
|
||||
@click="dismiss"
|
||||
>
|
||||
<span class="material-icons text-lg leading-none">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.alert-fade-leave-active {
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, max-height 0.25s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.alert-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
max-height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
image: { type: String, required: true },
|
||||
alt: { type: String, required: true },
|
||||
size: { type: String, default: 'md' },
|
||||
borderRadius: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const sizes = {
|
||||
xxs: 'h-5 w-5',
|
||||
xs: 'h-6 w-6',
|
||||
sm: 'h-9 w-9',
|
||||
md: 'h-11 w-11',
|
||||
lg: 'h-14 w-14',
|
||||
xl: 'h-[4.5rem] w-[4.5rem]',
|
||||
xxl: 'h-24 w-24',
|
||||
}
|
||||
|
||||
const radii = {
|
||||
'': 'rounded-xl',
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
xl: 'rounded-xl',
|
||||
'2xl':'rounded-2xl',
|
||||
full: 'rounded-full',
|
||||
}
|
||||
|
||||
const classes = computed(() => [
|
||||
'object-cover',
|
||||
sizes[props.size] ?? sizes.md,
|
||||
radii[props.borderRadius] ?? radii[''],
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img :src="image" :alt="alt" :class="classes" />
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator: v => ['sm', 'md', 'lg'].includes(v),
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'success',
|
||||
validator: v => ['primary','secondary','info','success','warning','error','danger','light','dark','white'].includes(v),
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'fill',
|
||||
validator: v => ['fill', 'gradient'].includes(v),
|
||||
},
|
||||
rounded: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const fill = {
|
||||
primary: 'bg-primary text-white',
|
||||
secondary: 'bg-secondary text-white',
|
||||
info: 'bg-info text-white',
|
||||
success: 'bg-success text-white',
|
||||
warning: 'bg-warning text-white',
|
||||
danger: 'bg-danger text-white',
|
||||
error: 'bg-danger text-white',
|
||||
light: 'bg-light text-dark',
|
||||
dark: 'bg-dark text-white',
|
||||
white: 'bg-white text-dark shadow-soft-xs',
|
||||
}
|
||||
|
||||
const grad = {
|
||||
primary: 'bg-gradient-primary shadow-primary text-white',
|
||||
secondary: 'bg-gradient-secondary shadow-secondary text-white',
|
||||
info: 'bg-gradient-info shadow-info text-white',
|
||||
success: 'bg-gradient-success shadow-success text-white',
|
||||
warning: 'bg-gradient-warning shadow-warning text-white',
|
||||
danger: 'bg-gradient-danger shadow-danger text-white',
|
||||
error: 'bg-gradient-danger shadow-danger text-white',
|
||||
light: 'bg-gradient-light shadow-soft-xs text-dark',
|
||||
dark: 'bg-gradient-dark shadow-dark text-white',
|
||||
white: 'bg-white shadow-soft-xs text-dark',
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-2 py-0.5 text-xs',
|
||||
md: 'px-2.5 py-1 text-xs',
|
||||
lg: 'px-3 py-1.5 text-sm',
|
||||
}
|
||||
|
||||
const classes = computed(() => [
|
||||
'inline-flex items-center font-medium',
|
||||
props.rounded ? 'rounded-full' : 'rounded-md',
|
||||
sizes[props.size],
|
||||
(props.variant === 'gradient' ? grad : fill)[props.color] ?? fill.success,
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="classes">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'contained',
|
||||
validator: v => ['contained', 'gradient', 'outline'].includes(v),
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: v => ['primary','secondary','info','success','warning','danger','error','light','white','dark','none'].includes(v),
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
validator: v => ['sm', 'md', 'lg'].includes(v),
|
||||
},
|
||||
fullWidth: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
// Static string maps — Tailwind scanner reads these to include the classes.
|
||||
const contained = {
|
||||
primary: 'bg-primary text-white',
|
||||
secondary: 'bg-secondary text-white',
|
||||
success: 'bg-success text-white',
|
||||
warning: 'bg-warning text-white',
|
||||
danger: 'bg-danger text-white',
|
||||
error: 'bg-danger text-white',
|
||||
info: 'bg-info text-white',
|
||||
light: 'bg-light text-dark',
|
||||
white: 'bg-white text-dark shadow-soft-sm',
|
||||
dark: 'bg-dark text-white',
|
||||
none: 'bg-transparent text-dark',
|
||||
}
|
||||
|
||||
const gradient = {
|
||||
primary: 'bg-gradient-primary shadow-primary text-white',
|
||||
secondary: 'bg-gradient-secondary shadow-secondary text-white',
|
||||
success: 'bg-gradient-success shadow-success text-white',
|
||||
warning: 'bg-gradient-warning shadow-warning text-white',
|
||||
danger: 'bg-gradient-danger shadow-danger text-white',
|
||||
error: 'bg-gradient-danger shadow-danger text-white',
|
||||
info: 'bg-gradient-info shadow-info text-white',
|
||||
light: 'bg-gradient-light shadow-soft-sm text-dark',
|
||||
white: 'bg-white shadow-soft-md text-dark',
|
||||
dark: 'bg-gradient-dark shadow-dark text-white',
|
||||
none: 'bg-transparent text-dark',
|
||||
}
|
||||
|
||||
const outline = {
|
||||
primary: 'border border-primary text-primary hover:bg-primary/10',
|
||||
secondary: 'border border-secondary text-secondary hover:bg-secondary/10',
|
||||
success: 'border border-success text-success hover:bg-success/10',
|
||||
warning: 'border border-warning text-warning hover:bg-warning/10',
|
||||
danger: 'border border-danger text-danger hover:bg-danger/10',
|
||||
error: 'border border-danger text-danger hover:bg-danger/10',
|
||||
info: 'border border-info text-info hover:bg-info/10',
|
||||
light: 'border border-gray-300 text-dark hover:bg-light',
|
||||
white: 'border border-white text-white hover:bg-white/10',
|
||||
dark: 'border border-dark text-dark hover:bg-dark/10',
|
||||
none: 'border border-gray-300 text-dark hover:bg-gray-100',
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-4 py-1.5 text-xs',
|
||||
md: 'px-6 py-2.5 text-sm',
|
||||
lg: 'px-8 py-3.5 text-base',
|
||||
}
|
||||
|
||||
const colorMap = { contained, gradient, outline }
|
||||
|
||||
const classes = computed(() => [
|
||||
'inline-flex items-center justify-center gap-2 font-medium rounded-lg cursor-pointer select-none',
|
||||
'transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0',
|
||||
colorMap[props.variant]?.[props.color] ?? '',
|
||||
sizes[props.size],
|
||||
props.fullWidth && 'w-full',
|
||||
props.disabled && 'opacity-60 pointer-events-none cursor-not-allowed',
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :class="classes" :disabled="disabled">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, default: '' },
|
||||
color: { type: String, default: 'dark' },
|
||||
modelValue: { type: Boolean, default: false },
|
||||
inputClass: { type: String, default: '' },
|
||||
labelClass: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
// Use CSS custom properties from @theme to drive accent-color.
|
||||
// This avoids generating per-color accent-* utilities.
|
||||
const accentStyle = computed(() => ({
|
||||
accentColor: props.color ? `var(--color-${props.color})` : undefined,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-start gap-2.5">
|
||||
<input
|
||||
:id="id"
|
||||
type="checkbox"
|
||||
:checked="modelValue"
|
||||
:class="['h-4 w-4 cursor-pointer rounded border-gray-300 transition-colors', inputClass]"
|
||||
:style="accentStyle"
|
||||
v-bind="$attrs"
|
||||
@change="$emit('update:modelValue', $event.target.checked)"
|
||||
/>
|
||||
<label
|
||||
v-if="$slots.default"
|
||||
:for="id"
|
||||
:class="['cursor-pointer select-none text-sm text-dark', labelClass]"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, default: '' },
|
||||
type: { type: String, default: 'text' },
|
||||
label: { type: [String, Object],default: '' },
|
||||
modelValue: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
size: { type: String, default: 'md' },
|
||||
error: { type: Boolean, default: false },
|
||||
success: { type: Boolean, default: false },
|
||||
isRequired: { type: Boolean, default: false },
|
||||
isDisabled: { type: Boolean, default: false },
|
||||
inputClass: { type: String, default: '' },
|
||||
icon: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const labelText = computed(() =>
|
||||
typeof props.label === 'string' ? props.label : props.label?.text ?? ''
|
||||
)
|
||||
const labelExtraClass = computed(() =>
|
||||
typeof props.label === 'object' ? props.label?.class ?? '' : ''
|
||||
)
|
||||
|
||||
const sizes = {
|
||||
sm: 'py-1.5 text-xs',
|
||||
md: 'py-2 text-sm',
|
||||
lg: 'py-2.5 text-base',
|
||||
}
|
||||
|
||||
const borderClass = computed(() => {
|
||||
if (props.error) return 'border-danger focus:border-danger focus:ring-1 focus:ring-danger'
|
||||
if (props.success) return 'border-success focus:border-success focus:ring-1 focus:ring-success'
|
||||
return 'border-gray-200 focus:border-primary focus:ring-1 focus:ring-primary'
|
||||
})
|
||||
|
||||
const inputClasses = computed(() => [
|
||||
'w-full rounded-lg border bg-white text-dark outline-none transition-colors placeholder:text-gray-400',
|
||||
props.icon ? 'pl-9 pr-3' : 'px-3',
|
||||
sizes[props.size] ?? sizes.md,
|
||||
borderClass.value,
|
||||
props.isDisabled && 'cursor-not-allowed opacity-60 bg-gray-50',
|
||||
props.inputClass,
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
v-if="labelText"
|
||||
:for="id"
|
||||
:class="['mb-1 block text-xs font-medium text-secondary', labelExtraClass]"
|
||||
>
|
||||
{{ labelText }}
|
||||
<span v-if="isRequired" class="text-danger">*</span>
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<span
|
||||
v-if="icon"
|
||||
class="material-icons pointer-events-none absolute left-2.5 top-1/2 -translate-y-1/2 text-base text-secondary"
|
||||
>
|
||||
{{ icon }}
|
||||
</span>
|
||||
|
||||
<input
|
||||
:id="id"
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:required="isRequired"
|
||||
:disabled="isDisabled"
|
||||
:class="inputClasses"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="error && typeof error === 'string'" class="mt-1 text-xs text-danger">
|
||||
{{ error }}
|
||||
</p>
|
||||
<p v-if="success && typeof success === 'string'" class="mt-1 text-xs text-success">
|
||||
{{ success }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
import { provide } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
color: { type: String, default: 'primary' },
|
||||
size: { type: String, default: 'md' },
|
||||
})
|
||||
|
||||
provide('mk-pagination-color', props.color)
|
||||
provide('mk-pagination-size', props.size)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="flex list-none items-center gap-1 p-0">
|
||||
<slot />
|
||||
</ul>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
import { inject, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
active: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
prev: { type: Boolean, default: false },
|
||||
next: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
defineEmits(['click'])
|
||||
|
||||
const color = inject('mk-pagination-color', 'primary')
|
||||
const size = inject('mk-pagination-size', 'md')
|
||||
|
||||
const activeClasses = {
|
||||
primary: 'bg-gradient-primary shadow-primary text-white',
|
||||
secondary: 'bg-gradient-secondary shadow-secondary text-white',
|
||||
success: 'bg-gradient-success shadow-success text-white',
|
||||
warning: 'bg-gradient-warning shadow-warning text-white',
|
||||
danger: 'bg-gradient-danger shadow-danger text-white',
|
||||
info: 'bg-gradient-info shadow-info text-white',
|
||||
dark: 'bg-gradient-dark shadow-dark text-white',
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-7 min-w-7 text-xs',
|
||||
md: 'h-9 min-w-9 text-sm',
|
||||
lg: 'h-11 min-w-11 text-base',
|
||||
}
|
||||
|
||||
const classes = computed(() => [
|
||||
'flex cursor-pointer select-none items-center justify-center rounded-lg px-2 font-medium transition-all duration-200',
|
||||
sizeClasses[size] ?? sizeClasses.md,
|
||||
props.active
|
||||
? (activeClasses[color] ?? activeClasses.primary)
|
||||
: 'text-secondary hover:bg-gray-100 hover:text-dark',
|
||||
props.disabled && 'pointer-events-none opacity-40',
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<button :class="classes" @click="$emit('click')">
|
||||
<span v-if="prev" class="material-icons text-base leading-none">chevron_left</span>
|
||||
<span v-else-if="next" class="material-icons text-base leading-none">chevron_right</span>
|
||||
<span v-else>{{ label }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'contained',
|
||||
validator: v => ['contained', 'gradient'].includes(v),
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: v => ['primary','secondary','info','success','warning','danger','error','light','dark'].includes(v),
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const contained = {
|
||||
primary: 'bg-primary',
|
||||
secondary: 'bg-secondary',
|
||||
info: 'bg-info',
|
||||
success: 'bg-success',
|
||||
warning: 'bg-warning',
|
||||
danger: 'bg-danger',
|
||||
error: 'bg-danger',
|
||||
light: 'bg-light',
|
||||
dark: 'bg-dark',
|
||||
}
|
||||
|
||||
const grad = {
|
||||
primary: 'bg-gradient-primary',
|
||||
secondary: 'bg-gradient-secondary',
|
||||
info: 'bg-gradient-info',
|
||||
success: 'bg-gradient-success',
|
||||
warning: 'bg-gradient-warning',
|
||||
danger: 'bg-gradient-danger',
|
||||
error: 'bg-gradient-danger',
|
||||
light: 'bg-gradient-light',
|
||||
dark: 'bg-gradient-dark',
|
||||
}
|
||||
|
||||
const barClass = computed(() =>
|
||||
(props.variant === 'gradient' ? grad : contained)[props.color] ?? contained.primary
|
||||
)
|
||||
|
||||
const pct = computed(() => Math.min(100, Math.max(0, props.value)))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full overflow-hidden rounded-full bg-gray-200" style="height: 6px">
|
||||
<div
|
||||
:class="barClass"
|
||||
class="h-full rounded-full transition-all duration-500"
|
||||
role="progressbar"
|
||||
:style="{ width: `${pct}%` }"
|
||||
:aria-valuenow="pct"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
route: { type: String, required: true },
|
||||
color: { type: String, required: true },
|
||||
component: { type: String, required: true },
|
||||
label: { type: String, default: '' },
|
||||
})
|
||||
|
||||
// Same contained map as MkButton — drives bg + text color.
|
||||
const colorClasses = {
|
||||
primary: 'bg-primary text-white hover:bg-primary/90',
|
||||
secondary: 'bg-secondary text-white hover:bg-secondary/90',
|
||||
success: 'bg-success text-white hover:bg-success/90',
|
||||
warning: 'bg-warning text-white hover:bg-warning/90',
|
||||
danger: 'bg-danger text-white hover:bg-danger/90',
|
||||
info: 'bg-info text-white hover:bg-info/90',
|
||||
dark: 'bg-dark text-white hover:bg-dark/90',
|
||||
white: 'bg-white text-dark shadow-soft-sm hover:shadow-soft-md',
|
||||
facebook: 'bg-[#3b5998] text-white hover:bg-[#344e86]',
|
||||
twitter: 'bg-[#1da1f2] text-white hover:bg-[#0d8fd9]',
|
||||
instagram: 'bg-[#e1306c] text-white hover:bg-[#c82761]',
|
||||
youtube: 'bg-[#ff0000] text-white hover:bg-[#e60000]',
|
||||
linkedin: 'bg-[#0077b5] text-white hover:bg-[#00669c]',
|
||||
github: 'bg-[#333333] text-white hover:bg-[#222222]',
|
||||
}
|
||||
|
||||
const classes = computed(() => [
|
||||
'inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200',
|
||||
colorClasses[props.color] ?? colorClasses.secondary,
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a :href="route" :class="classes" target="_blank" rel="noopener">
|
||||
<!--
|
||||
Renders a Font Awesome brand icon. Requires Font Awesome Free 6
|
||||
to be loaded in the project (e.g. via CDN or npm).
|
||||
The `component` prop maps to the FA icon slug: "facebook", "twitter", etc.
|
||||
-->
|
||||
<i class="fab" :class="`fa-${component}`" aria-hidden="true" />
|
||||
<span v-if="label">{{ label }}</span>
|
||||
</a>
|
||||
</template>
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, required: true },
|
||||
modelValue: { type: Boolean, default: false },
|
||||
color: { type: String, default: 'primary' },
|
||||
labelClass: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
// Drive the checked background via CSS custom property so we don't
|
||||
// need per-color utility classes for the track.
|
||||
const trackStyle = computed(() =>
|
||||
props.modelValue
|
||||
? { backgroundColor: `var(--color-${props.color})` }
|
||||
: {}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
:id="id"
|
||||
type="button"
|
||||
role="switch"
|
||||
:aria-checked="String(modelValue)"
|
||||
class="relative inline-flex h-5 w-10 shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-gray-300 transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1"
|
||||
:style="trackStyle"
|
||||
@click="$emit('update:modelValue', !modelValue)"
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow-sm transition-transform duration-200"
|
||||
:class="modelValue ? 'translate-x-5' : 'translate-x-0'"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<label
|
||||
v-if="$slots.default"
|
||||
:for="id"
|
||||
:class="['cursor-pointer select-none text-sm text-dark', labelClass]"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
|
||||
<div v-if="$slots.description" class="mt-0.5 text-xs text-secondary">
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, default: 'message' },
|
||||
rows: { type: Number, default: 4 },
|
||||
modelValue: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
labelClass: { type: String, default: '' },
|
||||
error: { type: Boolean, default: false },
|
||||
success: { type: Boolean, default: false },
|
||||
isDisabled: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const borderClass = computed(() => {
|
||||
if (props.error) return 'border-danger focus:border-danger focus:ring-1 focus:ring-danger'
|
||||
if (props.success) return 'border-success focus:border-success focus:ring-1 focus:ring-success'
|
||||
return 'border-gray-200 focus:border-primary focus:ring-1 focus:ring-primary'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
v-if="$slots.default"
|
||||
:for="id"
|
||||
:class="['mb-1 block text-xs font-medium text-secondary', labelClass]"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
:id="id"
|
||||
:rows="rows"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="isDisabled"
|
||||
:class="[
|
||||
'w-full resize-none rounded-lg border bg-white px-3 py-2 text-sm text-dark outline-none transition-colors placeholder:text-gray-400',
|
||||
borderClass,
|
||||
isDisabled && 'cursor-not-allowed opacity-60 bg-gray-50',
|
||||
]"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
+226
-78
@@ -1,31 +1,88 @@
|
||||
<script setup>
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import DashboardLayout from '../layouts/DashboardLayout.vue'
|
||||
|
||||
// ── Cover background ─────────────────────────────────────────
|
||||
const coverInputRef = ref(null)
|
||||
const coverAppliedUrl = ref(null) // committed
|
||||
const coverStagedUrl = ref(null) // pending preview
|
||||
|
||||
function onCoverPicked(e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
if (coverStagedUrl.value) URL.revokeObjectURL(coverStagedUrl.value)
|
||||
coverStagedUrl.value = URL.createObjectURL(file)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
function applyCover() {
|
||||
if (coverAppliedUrl.value) URL.revokeObjectURL(coverAppliedUrl.value)
|
||||
coverAppliedUrl.value = coverStagedUrl.value
|
||||
coverStagedUrl.value = null
|
||||
}
|
||||
|
||||
function cancelCover() {
|
||||
if (coverStagedUrl.value) URL.revokeObjectURL(coverStagedUrl.value)
|
||||
coverStagedUrl.value = null
|
||||
}
|
||||
|
||||
// ── Avatar ────────────────────────────────────────────────────
|
||||
const avatarInputRef = ref(null)
|
||||
const avatarAppliedUrl = ref(null)
|
||||
const avatarStagedUrl = ref(null)
|
||||
|
||||
function onAvatarPicked(e) {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
if (avatarStagedUrl.value) URL.revokeObjectURL(avatarStagedUrl.value)
|
||||
avatarStagedUrl.value = URL.createObjectURL(file)
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
function applyAvatar() {
|
||||
if (avatarAppliedUrl.value) URL.revokeObjectURL(avatarAppliedUrl.value)
|
||||
avatarAppliedUrl.value = avatarStagedUrl.value
|
||||
avatarStagedUrl.value = null
|
||||
}
|
||||
|
||||
function cancelAvatar() {
|
||||
if (avatarStagedUrl.value) URL.revokeObjectURL(avatarStagedUrl.value)
|
||||
avatarStagedUrl.value = null
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (coverStagedUrl.value) URL.revokeObjectURL(coverStagedUrl.value)
|
||||
if (coverAppliedUrl.value) URL.revokeObjectURL(coverAppliedUrl.value)
|
||||
if (avatarStagedUrl.value) URL.revokeObjectURL(avatarStagedUrl.value)
|
||||
if (avatarAppliedUrl.value)URL.revokeObjectURL(avatarAppliedUrl.value)
|
||||
})
|
||||
|
||||
// ── Static data ───────────────────────────────────────────────
|
||||
const stats = [
|
||||
{ label: 'Followers', value: '4,812' },
|
||||
{ label: 'Following', value: '294' },
|
||||
{ label: 'Projects', value: '32' },
|
||||
{ label: 'Projects', value: '32' },
|
||||
]
|
||||
|
||||
const socials = [
|
||||
{ icon: 'facebook', color: 'text-blue-600', label: 'Facebook' },
|
||||
{ icon: 'twitter', color: 'text-sky-500', label: 'Twitter' },
|
||||
{ icon: 'instagram', color: 'text-pink-500', label: 'Instagram' },
|
||||
{ icon: 'facebook', color: 'text-blue-600', label: 'Facebook' },
|
||||
{ icon: 'twitter', color: 'text-sky-500', label: 'Twitter' },
|
||||
{ icon: 'instagram', color: 'text-pink-500', label: 'Instagram' },
|
||||
]
|
||||
|
||||
const conversations = [
|
||||
{ initials: 'OP', color: 'primary', name: 'Olivia Park', preview: 'Hey, I loved the new component library!', time: '5m' },
|
||||
{ initials: 'JD', color: 'info', name: 'James Donovan', preview: 'Can you review my PR when you get a chance?', time: '22m' },
|
||||
{ initials: 'SR', color: 'success', name: 'Sofia Reyes', preview: 'The design tokens look great in production.', time: '1h' },
|
||||
{ initials: 'ML', color: 'warning', name: 'Marco Lin', preview: 'Pushed the icon fix — all shadows resolved.', time: '3h' },
|
||||
{ initials: 'AM', color: 'secondary', name: 'Ally Maria', preview: 'Updated the font preload entries in index.html.', time: '1d' },
|
||||
{ initials: 'OP', color: 'primary', name: 'Olivia Park', preview: 'Hey, I loved the new component library!', time: '5m' },
|
||||
{ initials: 'JD', color: 'info', name: 'James Donovan', preview: 'Can you review my PR when you get a chance?', time: '22m' },
|
||||
{ initials: 'SR', color: 'success', name: 'Sofia Reyes', preview: 'The design tokens look great in production.', time: '1h' },
|
||||
{ initials: 'ML', color: 'warning', name: 'Marco Lin', preview: 'Pushed the icon fix — all shadows resolved.', time: '3h' },
|
||||
{ initials: 'AM', color: 'secondary', name: 'Ally Maria', preview: 'Updated the font preload entries in index.html.',time: '1d' },
|
||||
]
|
||||
|
||||
const projects = [
|
||||
{ name: 'MK Design System', role: 'Lead', gradient: 'primary', members: ['PC', 'JD', 'SR'] },
|
||||
{ name: 'Dashboard Rework', role: 'Contributor', gradient: 'info', members: ['ML', 'AM'] },
|
||||
{ name: 'Icon Library', role: 'Owner', gradient: 'success', members: ['OP', 'JD'] },
|
||||
{ name: 'Typography Scale', role: 'Reviewer', gradient: 'warning', members: ['SR', 'ML', 'PC'] },
|
||||
{ name: 'MK Design System', role: 'Lead', gradient: 'primary', members: ['PC', 'JD', 'SR'] },
|
||||
{ name: 'Dashboard Rework', role: 'Contributor', gradient: 'info', members: ['ML', 'AM'] },
|
||||
{ name: 'Icon Library', role: 'Owner', gradient: 'success', members: ['OP', 'JD'] },
|
||||
{ name: 'Typography Scale', role: 'Reviewer', gradient: 'warning', members: ['SR', 'ML', 'PC'] },
|
||||
]
|
||||
|
||||
const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', AM: 'secondary', OP: 'danger' }
|
||||
@@ -34,37 +91,131 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
<template>
|
||||
<DashboardLayout>
|
||||
|
||||
<!-- Cover + avatar -->
|
||||
<div class="relative mb-20">
|
||||
<div class="h-48 overflow-hidden rounded-2xl bg-gradient-dark shadow-dark">
|
||||
<!-- Decorative grid -->
|
||||
<svg class="h-full w-full opacity-10" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="pgrid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="white" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#pgrid)" />
|
||||
</svg>
|
||||
<!-- Hidden file inputs -->
|
||||
<input ref="coverInputRef" type="file" accept="image/*" class="hidden" @change="onCoverPicked" />
|
||||
<input ref="avatarInputRef" type="file" accept="image/*" class="hidden" @change="onAvatarPicked" />
|
||||
|
||||
<!-- ── Cover + avatar ── -->
|
||||
<div class="relative mb-16">
|
||||
|
||||
<!-- Cover -->
|
||||
<div class="group relative h-48 overflow-hidden rounded-2xl bg-gradient-dark shadow-dark">
|
||||
|
||||
<!-- Background: applied image or default gradient+grid -->
|
||||
<template v-if="coverStagedUrl || coverAppliedUrl">
|
||||
<img
|
||||
:src="coverStagedUrl ?? coverAppliedUrl"
|
||||
class="h-full w-full object-cover"
|
||||
alt="Cover preview"
|
||||
/>
|
||||
<!-- dim overlay so controls stay readable -->
|
||||
<div class="absolute inset-0 bg-black/20" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg class="h-full w-full opacity-10" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<pattern id="pgrid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="white" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#pgrid)" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<!-- Change bg button — top-right, always accessible -->
|
||||
<button
|
||||
class="absolute right-3 top-3 flex items-center gap-1.5 rounded-lg bg-black/40 px-3 py-1.5 text-xs font-medium text-white opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100"
|
||||
@click="coverInputRef.click()"
|
||||
>
|
||||
<span class="material-icons text-sm leading-none">image</span>
|
||||
Change bg
|
||||
</button>
|
||||
|
||||
<!-- Apply / Cancel strip — bottom of cover, only when staged -->
|
||||
<Transition name="strip">
|
||||
<div
|
||||
v-if="coverStagedUrl"
|
||||
class="absolute inset-x-0 bottom-0 flex items-center justify-end gap-2 bg-black/50 px-4 py-2.5 backdrop-blur-sm"
|
||||
>
|
||||
<span class="mr-auto text-xs text-white/70">New background staged</span>
|
||||
<button
|
||||
class="rounded-lg px-3 py-1.5 text-xs font-medium text-white/70 hover:text-white transition-colors"
|
||||
@click="cancelCover"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-gradient-primary px-4 py-1.5 text-xs font-medium text-white shadow-primary"
|
||||
@click="applyCover"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<!-- Avatar -->
|
||||
<div class="absolute -bottom-14 left-8 flex h-28 w-28 items-center justify-center rounded-2xl bg-gradient-primary text-4xl font-bold text-white shadow-primary ring-4 ring-white">
|
||||
EC
|
||||
<div class="absolute -bottom-14 left-8">
|
||||
<div class="group/av relative h-28 w-28">
|
||||
|
||||
<!-- Avatar display: photo or initials -->
|
||||
<div
|
||||
class="h-28 w-28 overflow-hidden rounded-2xl ring-4 ring-white shadow-primary"
|
||||
:class="avatarAppliedUrl || avatarStagedUrl ? '' : 'bg-gradient-primary'"
|
||||
>
|
||||
<img
|
||||
v-if="avatarStagedUrl || avatarAppliedUrl"
|
||||
:src="avatarStagedUrl ?? avatarAppliedUrl"
|
||||
class="h-full w-full object-cover"
|
||||
alt="Avatar preview"
|
||||
/>
|
||||
<div v-else class="flex h-full w-full items-center justify-center text-4xl font-bold text-white">
|
||||
EC
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change photo overlay — appears on avatar hover -->
|
||||
<button
|
||||
class="absolute inset-0 flex flex-col items-center justify-center gap-1 rounded-2xl bg-black/50 opacity-0 transition-opacity group-hover/av:opacity-100"
|
||||
@click="avatarInputRef.click()"
|
||||
>
|
||||
<span class="material-icons text-xl text-white">photo_camera</span>
|
||||
<span class="text-xs font-medium text-white">Change</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avatar Apply / Cancel — sits just below the avatar -->
|
||||
<Transition name="slide-down">
|
||||
<div v-if="avatarStagedUrl" class="mb-4 flex items-center gap-2 pl-8">
|
||||
<span class="text-xs text-secondary">New photo staged</span>
|
||||
<button
|
||||
class="rounded-lg border border-gray-200 px-3 py-1 text-xs font-medium text-secondary hover:text-dark transition-colors"
|
||||
@click="cancelAvatar"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="rounded-lg bg-gradient-primary px-4 py-1 text-xs font-medium text-white shadow-primary"
|
||||
@click="applyAvatar"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="mb-4 h-7" />
|
||||
</Transition>
|
||||
|
||||
<!-- ── Main grid ── -->
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
|
||||
<!-- Left column -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Identity card -->
|
||||
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
|
||||
<h4 class="text-xl font-bold text-dark">Esthera Carter</h4>
|
||||
<p class="mt-0.5 text-sm text-secondary">Lead Designer · San Francisco, CA</p>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="mt-5 grid grid-cols-3 divide-x divide-gray-100 rounded-xl border border-gray-100">
|
||||
<div v-for="s in stats" :key="s.label" class="flex flex-col items-center py-3">
|
||||
<span class="text-lg font-bold text-dark">{{ s.value }}</span>
|
||||
@@ -72,13 +223,11 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bio -->
|
||||
<p class="mt-5 text-sm leading-relaxed text-secondary">
|
||||
Building design systems and component libraries. Obsessed with typography,
|
||||
spacing, and making sure every pixel is intentional.
|
||||
</p>
|
||||
|
||||
<!-- Socials -->
|
||||
<div class="mt-5 flex items-center gap-3">
|
||||
<a
|
||||
v-for="s in socials"
|
||||
@@ -92,14 +241,13 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conversations -->
|
||||
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
|
||||
<h6 class="mb-4 text-sm font-bold text-dark">Conversations</h6>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="c in conversations"
|
||||
:key="c.name"
|
||||
class="flex items-start gap-3 cursor-pointer group"
|
||||
class="group flex cursor-pointer items-start gap-3"
|
||||
>
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-xs font-bold text-white"
|
||||
@@ -107,9 +255,9 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
>
|
||||
{{ c.initials }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-dark group-hover:text-primary transition-colors">{{ c.name }}</p>
|
||||
<p class="text-xs text-secondary truncate">{{ c.preview }}</p>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-dark transition-colors group-hover:text-primary">{{ c.name }}</p>
|
||||
<p class="truncate text-xs text-secondary">{{ c.preview }}</p>
|
||||
</div>
|
||||
<span class="shrink-0 text-xs text-secondary">{{ c.time }}</span>
|
||||
</div>
|
||||
@@ -121,7 +269,6 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
<!-- Right column -->
|
||||
<div class="space-y-6 lg:col-span-2">
|
||||
|
||||
<!-- Projects -->
|
||||
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h6 class="text-sm font-bold text-dark">Projects</h6>
|
||||
@@ -136,9 +283,7 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
:key="p.name"
|
||||
class="group relative overflow-hidden rounded-xl border border-gray-100 p-4 transition-shadow hover:shadow-soft-md"
|
||||
>
|
||||
<!-- Gradient accent bar -->
|
||||
<div class="absolute left-0 top-0 h-full w-1 rounded-l-xl" :class="`bg-gradient-${p.gradient}`" />
|
||||
|
||||
<div class="pl-3">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
@@ -154,19 +299,15 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
<span class="material-icons text-base leading-none">more_vert</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Member avatars -->
|
||||
<div class="mt-3 flex items-center">
|
||||
<div class="flex -space-x-2">
|
||||
<div
|
||||
v-for="m in p.members"
|
||||
:key="m"
|
||||
class="flex h-7 w-7 items-center justify-center rounded-lg text-xs font-bold text-white ring-2 ring-white"
|
||||
:class="`bg-gradient-${memberColor[m]} shadow-${memberColor[m]}`"
|
||||
:title="m"
|
||||
>
|
||||
{{ m }}
|
||||
</div>
|
||||
<div class="mt-3 flex -space-x-2">
|
||||
<div
|
||||
v-for="m in p.members"
|
||||
:key="m"
|
||||
class="flex h-7 w-7 items-center justify-center rounded-lg text-xs font-bold text-white ring-2 ring-white"
|
||||
:class="`bg-gradient-${memberColor[m]} shadow-${memberColor[m]}`"
|
||||
:title="m"
|
||||
>
|
||||
{{ m }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,7 +315,6 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
|
||||
<h6 class="mb-5 text-sm font-bold text-dark">Profile Settings</h6>
|
||||
<div class="space-y-4">
|
||||
@@ -182,50 +322,31 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-secondary">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Esthera"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||
/>
|
||||
<input type="text" value="Esthera" class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-secondary">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value="Carter"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||
/>
|
||||
<input type="text" value="Carter" class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-secondary">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
value="esthera@mkdesign.dev"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||
/>
|
||||
<input type="email" value="esthera@mkdesign.dev" class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-secondary">Location</label>
|
||||
<input
|
||||
type="text"
|
||||
value="San Francisco, CA"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors"
|
||||
/>
|
||||
<input type="text" value="San Francisco, CA" class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-secondary">Bio</label>
|
||||
<textarea
|
||||
rows="3"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors resize-none"
|
||||
>Building design systems and component libraries. Obsessed with typography, spacing, and making sure every pixel is intentional.</textarea>
|
||||
<textarea rows="3" class="w-full resize-none rounded-lg border border-gray-200 px-3 py-2 text-sm text-dark outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary">Building design systems and component libraries. Obsessed with typography, spacing, and making sure every pixel is intentional.</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button class="rounded-lg bg-gradient-primary px-6 py-2.5 text-sm font-medium text-white shadow-primary hover:shadow-soft-md transition-shadow">
|
||||
<button class="rounded-lg bg-gradient-primary px-6 py-2.5 text-sm font-medium text-white shadow-primary transition-shadow hover:shadow-soft-md">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
@@ -238,3 +359,30 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
|
||||
|
||||
</DashboardLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.strip-enter-active,
|
||||
.strip-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.strip-enter-from,
|
||||
.strip-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.slide-down-enter-active,
|
||||
.slide-down-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.slide-down-enter-from,
|
||||
.slide-down-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
}
|
||||
.slide-down-enter-to,
|
||||
.slide-down-leave-from {
|
||||
max-height: 4rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user