adding components

This commit is contained in:
2026-06-01 17:09:26 -06:00
parent 79a8720fb0
commit 0b1ab9261e
12 changed files with 681 additions and 0 deletions
+79
View File
@@ -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>
+40
View File
@@ -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>
+67
View File
@@ -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>
+89
View File
@@ -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>
+40
View File
@@ -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>
+89
View File
@@ -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>
+17
View File
@@ -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>
+51
View File
@@ -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>
+64
View File
@@ -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>
+45
View File
@@ -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>
+51
View File
@@ -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>
+49
View File
@@ -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>