Compare commits

...

8 Commits

Author SHA1 Message Date
brian fdf0d5865b adding some more cards 2026-06-02 08:34:59 -06:00
brian 8ad6132dfa tweaking minor stuff 2026-06-01 17:37:12 -06:00
brian db2ddadff3 finished up the components, added showcase view, and documentation 2026-06-01 17:32:42 -06:00
brian d34d61458b adding a table component 2026-06-01 17:25:47 -06:00
brian 9dfe4a9a01 adding layout components 2026-06-01 17:21:19 -06:00
brian 23a0d8dc2f adding some cards and a composable 2026-06-01 17:15:00 -06:00
brian 0b1ab9261e adding components 2026-06-01 17:09:26 -06:00
brian 79a8720fb0 scaffolded avatar and bg image uploaders 2026-06-01 17:01:29 -06:00
47 changed files with 3630 additions and 87 deletions
+117 -3
View File
@@ -1,5 +1,119 @@
# Vue Material Kit Design System # MK Design System
This is a fork/derivative of [Creative Tim's Vue Material Kit 2](https://www.creative-tim.com/product/vue-material-kit), source found [here](https://github.com/creativetimofficial/vue-material-kit). A Vue 3 + Tailwind CSS v4 design system derived from [Creative Tim's Vue Material Kit 2](https://github.com/creativetimofficial/vue-material-kit). Built to be handed off to another Claude instance for consistent UI work across projects.
I am going to iterate over it with an AI agent and extract styles and functionality into a design system that can be plugged in to another project. The end goal is to have a working directory than can be handed off to another AI agent that is used when building a new site to keep a consistent and clean design. **30+ components** · **Design tokens in CSS** · **Roboto variable font** · **Material Icons Round**
---
## Using in your project
### Option A — Copy components (simplest)
1. Copy `src/components/`, `src/composables/`, and `src/style.css` into your project.
2. Ensure your project has Tailwind CSS v4 with `@tailwindcss/vite`. Your `style.css` must contain the `@import "tailwindcss"` line and the full `@theme` block from this project's `src/style.css`.
3. Get the font files (4 woff2 files — see **Fonts** below) and place them in `public/fonts/`.
4. Import components directly:
```js
import MkButton from '@/components/MkButton.vue'
import MkInput from '@/components/MkInput.vue'
```
### Option B — Build as a library
```bash
pnpm build:lib
```
This outputs `dist/mk-design-system.es.js`, `dist/mk-design-system.cjs.js`, and `dist/style.css`.
In the consuming project:
```js
// main.js — global registration
import { MkDesignSystem } from './mk-design-system.es.js'
import './mk-design-system.css' // design tokens + @font-face
app.use(MkDesignSystem)
```
Or import components individually:
```js
import { MkButton, MkInput, MkNavbar } from './mk-design-system.es.js'
```
---
## Development
```bash
pnpm install
pnpm dev # dashboard + showcase at localhost:5173
pnpm build # production app build → dist/
pnpm build:lib # library build → dist/mk-design-system.*
```
---
## Fonts
Place these woff2 files in `public/fonts/` before running the dev server:
```
public/fonts/roboto/
Roboto-VariableFont_wdth-wght.woff2
Roboto-Italic-VariableFont_wdth-wght.woff2
public/fonts/roboto-slab/
RobotoSlab-VariableFont_wght.woff2
public/fonts/material-icons-round/
material-icons-round-latin-400-normal.woff2
```
Download script (run from `/app`):
```bash
# Roboto (official Google Fonts variable build)
curl -L "https://github.com/google/fonts/raw/main/apache/roboto/Roboto%5Bwdth%2Cwght%5D.ttf" \
-o public/fonts/roboto/Roboto-VariableFont_wdth-wght.woff2
# Material Icons Round
curl -L "https://cdn.jsdelivr.net/npm/@fontsource/material-icons-round@5/files/material-icons-round-latin-400-normal.woff2" \
-o public/fonts/material-icons-round/material-icons-round-latin-400-normal.woff2
```
---
## Design reference
| File | Purpose |
|------|---------|
| `design-system.md` | Token reference, component patterns, 12 quick-start rules |
| `conformity.md` | 17-rule checklist — feed this + generated code to Claude to score conformity |
| `tokens.json` | Machine-readable design token values |
| `src/style.css` | Live source of truth — `@theme` block defines all tokens |
---
## For Claude
When starting a new project using this design system, provide Claude with:
1. `design-system.md` — the rules to follow
2. `conformity.md` — to score and verify generated code
3. The component list from `src/index.js` — so it knows what's available
Prompt template:
> "Use the MK Design System (design-system.md attached). Available components are listed in src/index.js. Build [feature]. After generating, score it against conformity.md."
---
## Notes
- `MkSocialButton` renders Font Awesome brand icons (`fab fa-*`). Load Font Awesome Free 6 externally if social icons are needed.
- `MkRotatingCard` requires its parent to set an explicit height (e.g. `style="height: 22rem"`).
- `MkNavbar` scroll-blur is automatic in `transparent` mode — no JS required.
+38
View File
@@ -0,0 +1,38 @@
# MK Design System — Conformity Checklist
## How to Use
Provide this file plus the code to review to Claude with this prompt:
"Review the following code against the conformity checklist below. For each rule, output PASS or FAIL with a specific code callout. Then provide a conformity score."
## Rules
### Visual Token Conformity
- [ ] **C-V1**: Colors use semantic Tailwind classes (`bg-primary`, `text-success`, `border-danger`) — no raw hex values in HTML class attributes or `:class` bindings
- [ ] **C-V2**: No hardcoded hex in inline `:style` attributes or `@layer` CSS rules; use `var(--color-*)` or Tailwind classes instead
- [ ] **C-V3**: Spacing uses Tailwind scale classes (`mt-4`, `px-6`, `py-16`) — no inline `style="margin: ..."` or `style="padding: ..."`
- [ ] **C-V4**: Shadows use `shadow-soft-*` or `shadow-{color}` utilities — no custom `box-shadow` values in class attributes
- [ ] **C-V5**: Border radius uses `rounded-*` utilities (`rounded-lg`, `rounded-2xl`, `rounded-full`) — no inline `border-radius` style values
### Component Conformity
- [ ] **C-C1**: All buttons use `MkButton` component or the manual gradient pattern (`bg-gradient-{color} shadow-{color} text-white rounded-lg px-6 py-2.5`); dark-section buttons use `color="white"`; icon-only buttons are square with `p-2`
- [ ] **C-C2**: Cards follow `rounded-2xl bg-white shadow-soft-md` base; card image headers use `-mt-6 mx-4 overflow-hidden rounded-xl` floating pattern; interactive cards use `move-on-hover`
- [ ] **C-C3**: Form inputs use `MkInput` with `v-model`; raw `<input>` elements (if used) must have `rounded-lg border border-gray-200 px-3 py-2 focus:border-primary focus:ring-1 focus:ring-primary`
- [ ] **C-C4**: Navigation uses `MkNavbar` with `navItems` prop; custom navbars must replicate scroll-blur behavior (`bg-white/90 backdrop-blur-sm`) and responsive collapse
- [ ] **C-C5**: Alerts use `MkAlert` component or `rounded-lg px-4 py-3 text-sm bg-{color} text-white` — no plain unstyled `<div role="alert">`
### Layout Conformity
- [ ] **C-L1**: Marketing pages use `max-w-7xl mx-auto px-4` as the page container — not full-bleed or custom `max-width` values
- [ ] **C-L2**: Responsive breakpoints use Tailwind prefixes (`sm:`, `md:`, `lg:`, `xl:`) — no CSS media queries in `<style>` blocks unless unavoidable
- [ ] **C-L3**: Content grids use `grid gap-6` with `grid-cols-{n}` and responsive column modifiers — not `flex flex-wrap` with manual width percentages
- [ ] **C-L4**: Flex layouts use Tailwind utilities (`flex`, `items-center`, `justify-between`, `gap-4`) — no `display: flex` in inline styles
### Naming Conformity
- [ ] **C-N1**: CSS custom properties follow the `@theme` naming convention: `--color-*`, `--shadow-*`, `--radius-*`, `--font-*`, `--background-image-*`
- [ ] **C-N2**: No Bootstrap class names present (`.container`, `.row`, `.col-*`, `.btn`, `.badge`, `.card`, `.navbar`, `.form-control`, etc.)
- [ ] **C-N3**: Vue components use PascalCase with `Mk` prefix (`MkButton`, `MkNavbar`, `MkDefaultCounterCard`); custom utility classes use kebab-case (`move-on-hover`, `text-gradient`)
## Scoring Guide
- 1517 rules pass → **Fully conformant** — ready for production
- 1214 rules pass → **Mostly conformant** — minor cleanup needed
- 811 rules pass → **Partially conformant** — significant drift, review required
- Below 8 → **Non-conformant** — major rework needed before shipping
+212
View File
@@ -0,0 +1,212 @@
# MK Design System
## Overview
- **Framework**: Vue 3.5+
- **Styling**: Tailwind CSS v4 (CSS-first config, no tailwind.config.js)
- **Font**: Roboto variable (self-hosted woff2), Roboto Slab, Material Icons Round
- **Icons**: Material Icons Round via `.material-icons` class
- **Color mode**: Light (CSS custom properties ready for dark mode extension)
- **Token source**: `tokens.json``src/style.css` `@theme` block is the live source of truth
---
## Visual Tokens
### Colors
| Token | CSS var | Hex | Usage |
|-------|---------|-----|-------|
| primary | `--color-primary` | #e91e63 | Buttons, links, key UI (pink/rose) |
| secondary | `--color-secondary` | #7b809a | Secondary actions, muted text |
| success | `--color-success` | #4caf50 | Positive states |
| warning | `--color-warning` | #fb8c00 | Warning states |
| danger | `--color-danger` | #f44335 | Error / destructive |
| info | `--color-info` | #1a73e8 | Informational |
| light | `--color-light` | #f0f2f5 | Page backgrounds |
| dark | `--color-dark` | #344767 | Dark text / navy |
All generate `bg-*`, `text-*`, `border-*`, `ring-*` utilities automatically.
### Gradients
195° linear gradients — this angle is the signature of the kit. **Do not change it.**
| Class | Direction |
|-------|-----------|
| `bg-gradient-primary` | #EC407A#D81B60 |
| `bg-gradient-success` | #66bb6a#43a047 |
| `bg-gradient-info` | #49a3f1#1a73e8 |
| `bg-gradient-dark` | #42424a#191919 |
| …and 4 more (secondary/warning/danger/light) | |
Pair gradient classes with their colored shadow for the floating-card effect:
```html
<div class="bg-gradient-primary shadow-primary rounded-xl p-4 text-white">
```
### Shadows
| Class | Use |
|-------|-----|
| `shadow-soft-xs` | Subtle hover feedback |
| `shadow-soft-sm` | Default card lift |
| `shadow-soft-md` | Standard card |
| `shadow-soft-lg` | Elevated panels |
| `shadow-blur` | Frosted-glass containers |
| `shadow-primary` / `shadow-success` / … | Colored glow — always paired with matching gradient |
### Typography
| Class | Value | Use |
|-------|-------|-----|
| `font-sans` | Roboto variable | Body and UI |
| `font-serif` | Roboto Slab variable | Display/editorial |
| `font-mono` | SFMono / system | Code |
| `font-light` | 300 | Large display headings |
| `font-normal` | 400 | Body copy |
| `font-medium` | 500 | Labels, nav |
| `font-bold` | 700 | Headings, emphasis |
| `font-black` | 900 | Counter cards, hero numbers |
### Spacing
Use Tailwind's default scale. Common values: `p-4` (1rem), `p-6` (1.5rem), `py-16` (4rem) for section padding. No custom spacing tokens.
### Border Radius
| Class | Value | Use |
|-------|-------|-----|
| `rounded-xs` | 0.1rem | Micro elements |
| `rounded-lg` | 0.5rem | Inputs, small cards |
| `rounded-xl` | 0.75rem | Card images, avatars |
| `rounded-2xl` | 1rem | Cards, panels |
| `rounded-full` | 9999px | Badges (pill), avatars (circle) |
---
## Component Patterns
### Button
```html
<!-- Via component (recommended) -->
<MkButton variant="gradient" color="primary">Get Started</MkButton>
<MkButton variant="outline" color="primary" size="sm">Learn More</MkButton>
<!-- Manual (dark sections) -->
<button class="bg-white text-dark rounded-lg px-6 py-2.5 text-sm font-medium">
Contact
</button>
```
**Variants**: `contained` (flat fill) · `gradient` (195° gradient + colored shadow) · `outline`
**Colors**: primary / secondary / success / warning / danger / info / light / white / dark
**Sizes**: sm · md (default) · lg
**Do**: Use `variant="gradient"` for primary CTAs. Use `color="white"` on dark/gradient backgrounds.
**Don't**: Use `bg-primary` manually when `MkButton` is available.
### Card (marketing)
```html
<div class="rounded-2xl bg-white shadow-soft-md">
<!-- Floating image header (overlaps card top) -->
<div class="-mt-6 mx-4 z-10 overflow-hidden rounded-xl shadow-soft-lg">
<img src="..." class="h-48 w-full object-cover" />
</div>
<div class="px-6 pb-6 pt-4">
<h5 class="font-semibold text-dark">Title</h5>
<p class="text-sm text-secondary">Body text.</p>
</div>
</div>
```
### Navbar
```html
<MkNavbar
:brand="{ name: 'My App', route: '/' }"
:nav-items="[
{ label: 'Home', href: '/' },
{ label: 'Pages', icon: 'dashboard', children: [
{ label: 'About', href: '/about', description: 'Our story' },
]},
]"
:action="{ label: 'Get Started', href: '/signup', color: 'primary' }"
:transparent="true"
/>
```
Scroll-triggered frosted glass is automatic in transparent mode.
### Form Inputs
```html
<MkInput label="Full Name" v-model="name" placeholder="e.g. Jane Doe" />
<MkInput label="Email" type="email" v-model="email" :error="true" />
<MkInput label="Search" icon="search" v-model="query" />
<MkTextArea label="Message" v-model="msg" :rows="4" />
<MkCheckbox id="terms" v-model="agreed" color="primary">I agree</MkCheckbox>
<MkSwitch id="notify" v-model="notify" color="primary">Email notifications</MkSwitch>
```
### Hero Section
```html
<MkHeader
image="/img/hero.jpg"
:title="{ text: 'Build Faster', variant: 'h1' }"
description="A design system for Vue 3 and Tailwind v4."
mask="dark"
:mask-opacity="0.55"
:center="true"
min-height="80vh"
>
<MkButton variant="gradient" color="primary" class="mt-6">Get Started</MkButton>
</MkHeader>
```
### Gradient text
```html
<h1 class="text-gradient bg-gradient-primary text-5xl font-black">42,000+</h1>
```
`text-gradient` clips the background image to the text shape. Always pair with a `bg-gradient-*` class.
### Move-on-hover (interactive cards)
```html
<div class="move-on-hover rounded-2xl bg-white shadow-soft-md p-6">
<!-- Lifts 4px on hover with ease transition -->
</div>
```
---
## Layout & Grid
| Element | Classes |
|---------|---------|
| Page container | `max-w-7xl mx-auto px-4` |
| Section spacing | `py-16` (4rem) or `py-24` (6rem) for hero |
| Grid (3-col) | `grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3` |
| Grid (4-col) | `grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-4` |
| Flex row | `flex items-center gap-4` |
| Flex wrap | `flex flex-wrap gap-3` |
Breakpoints (Tailwind v4 defaults): `sm` 640px · `md` 768px · `lg` 1024px · `xl` 1280px.
---
## Naming Conventions
- **CSS custom properties**: `--color-*`, `--shadow-*`, `--radius-*`, `--font-*`, `--background-image-*`
- **Vue components**: PascalCase with `Mk` prefix — `MkButton`, `MkNavbar`, `MkDefaultCounterCard`
- **Custom CSS classes**: kebab-case — `move-on-hover`, `text-gradient`, `rotating-card-container`
- **No Bootstrap class names** anywhere in the codebase
---
## Quick-Start Rules for Code Generation
When generating code using this design system:
1. Use Tailwind v4 utility classes only — no Bootstrap, no custom SCSS
2. Reference colors via semantic class names (`bg-primary`, `text-success`) — never raw hex in HTML
3. Spacing: use Tailwind scale (`mt-4`, `px-6`, `py-16`) — no inline `style` for spacing
4. Primary CTAs: `MkButton variant="gradient" color="primary"` or `bg-gradient-primary shadow-primary text-white rounded-lg px-6 py-2.5`
5. Dark-section buttons: `color="white"` (white background, dark text) — not `color="light"`
6. Cards: `rounded-2xl bg-white shadow-soft-md` as the base; floating headers use `-mt-6 mx-4 rounded-xl overflow-hidden`
7. Page layout: `max-w-7xl mx-auto px-4` container with `grid gap-6` inside
8. Form fields: always use `MkInput`, `MkTextArea`, `MkCheckbox`, `MkSwitch` — not raw `<input>`
9. Images inside cards: `rounded-xl object-cover` for consistent soft appearance
10. Interactive cards: add `move-on-hover` class for hover lift effect
11. Use `Mk*` components throughout — consult `src/components/` for available primitives
12. Navbar scroll blur is automatic; pass `navItems` as data, not hardcoded HTML
+1
View File
@@ -6,6 +6,7 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"build:lib": "BUILD_TARGET=lib vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
+81
View File
@@ -0,0 +1,81 @@
<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;
/* Must be a numeric value — CSS cannot transition from 'auto' to 0 */
max-height: 200px;
}
.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>
+139
View File
@@ -0,0 +1,139 @@
<script setup>
defineProps({
headers: {
type: Array,
default: () => ['Author', 'Function', 'Status', 'Employed', 'Action'],
},
/**
* Each row: {
* image?: string — avatar URL (falls back to initials)
* initials?: string — shown when no image
* color?: string — gradient color for initials avatar
* name: string
* email: string
* position: [string, string] — [label, sublabel]
* status: boolean — true = Online, false = Offline
* date: string
* action: { label, route }
* }
*/
rows: {
type: Array,
required: true,
},
})
const avatarGradients = {
primary: 'bg-gradient-primary shadow-primary',
secondary: 'bg-gradient-secondary shadow-secondary',
success: 'bg-gradient-success shadow-success',
warning: 'bg-gradient-warning shadow-warning',
danger: 'bg-gradient-danger shadow-danger',
info: 'bg-gradient-info shadow-info',
dark: 'bg-gradient-dark shadow-dark',
}
</script>
<template>
<div class="overflow-hidden rounded-2xl bg-white shadow-soft-md">
<!-- Optional slot for a card header (title, actions, etc.) -->
<div v-if="$slots.header" class="border-b border-gray-100 px-6 py-4">
<slot name="header" />
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-sm">
<thead>
<tr class="border-b border-gray-100">
<th
v-for="(header, i) in headers"
:key="header"
class="px-6 py-3 text-xs font-bold uppercase tracking-wide text-secondary"
:class="i > 1 ? 'text-center' : ''"
>
{{ header }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, i) in rows"
:key="i"
:class="i < rows.length - 1 ? 'border-b border-gray-100' : ''"
>
<!-- Author: avatar + name + email -->
<td class="px-6 py-3">
<div class="flex items-center gap-3">
<!-- Photo avatar -->
<img
v-if="row.image"
:src="row.image"
:alt="row.name"
class="h-9 w-9 shrink-0 rounded-xl object-cover"
/>
<!-- Initials avatar -->
<div
v-else
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-xs font-bold text-white"
:class="avatarGradients[row.color] ?? avatarGradients.dark"
>
{{ row.initials ?? row.name?.slice(0, 2).toUpperCase() }}
</div>
<div>
<p class="font-medium text-dark">{{ row.name }}</p>
<p class="text-xs text-secondary">{{ row.email }}</p>
</div>
</div>
</td>
<!-- Position: two-line label -->
<td class="px-6 py-3">
<p class="font-medium text-dark">{{ row.position?.[0] ?? '—' }}</p>
<p class="text-xs text-secondary">{{ row.position?.[1] ?? '' }}</p>
</td>
<!-- Status badge -->
<td class="px-6 py-3 text-center">
<span
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium"
:class="row.status
? 'bg-success/10 text-success'
: 'bg-gray-100 text-secondary'"
>
<span
class="h-1.5 w-1.5 rounded-full"
:class="row.status ? 'bg-success' : 'bg-gray-400'"
/>
{{ row.status ? 'Online' : 'Offline' }}
</span>
</td>
<!-- Date -->
<td class="px-6 py-3 text-center text-xs text-secondary">
{{ row.date }}
</td>
<!-- Action -->
<td class="px-6 py-3 text-center">
<a
:href="row.action?.route ?? '#'"
class="text-xs font-medium text-secondary no-underline transition-colors hover:text-primary"
>
{{ row.action?.label ?? 'Edit' }}
</a>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Optional slot for pagination or footer -->
<div v-if="$slots.footer" class="border-t border-gray-100 px-6 py-4">
<slot name="footer" />
</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>
@@ -0,0 +1,52 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
image: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
action: {
type: Object,
default: () => ({ route: '#', label: 'Read more', color: 'white' }),
},
})
const linkColors = {
white: 'text-white',
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
warning: 'text-warning',
danger: 'text-danger',
info: 'text-info',
dark: 'text-dark',
}
const linkClass = computed(() => linkColors[props.action.color] ?? linkColors.white)
</script>
<template>
<div class="group relative h-64 cursor-pointer overflow-hidden rounded-2xl">
<!-- Background image with subtle zoom on hover -->
<div
class="absolute inset-0 bg-cover bg-center transition-transform duration-500 group-hover:scale-105"
:style="{ backgroundImage: `url(${image})` }"
/>
<!-- Gradient overlay bottom-heavy for text legibility -->
<div class="absolute inset-0 bg-gradient-to-t from-black/75 via-black/30 to-black/10" />
<!-- Content -->
<div class="absolute inset-0 flex flex-col justify-end p-6">
<h2 class="text-xl font-bold text-white leading-snug">{{ title }}</h2>
<p class="mt-1 text-sm text-white/80 line-clamp-2">{{ description }}</p>
<a
:href="action.route"
class="mt-3 inline-flex items-center gap-1 text-sm font-medium transition-opacity hover:opacity-80"
:class="linkClass"
>
{{ action.label }}
<span class="material-icons text-base leading-none">arrow_forward</span>
</a>
</div>
</div>
</template>
@@ -0,0 +1,53 @@
<script setup>
defineProps({
image: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
action: {
type: Object,
default: () => ({ color: 'success', label: 'Find Out More', route: '#' }),
},
})
// action.color is a design-system color name: 'success', 'primary', etc.
const btnClasses = {
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',
light: 'bg-gradient-light shadow-soft-sm text-dark',
}
</script>
<template>
<div class="rounded-2xl bg-white shadow-soft-md overflow-visible">
<!-- Floating image header overlaps the card top -->
<div class="relative -mt-6 mx-4 z-10 overflow-hidden rounded-xl shadow-soft-lg">
<a :href="action.route" class="block">
<img
:src="image"
:alt="title"
class="h-48 w-full object-cover transition-transform duration-500 hover:scale-105"
/>
</a>
</div>
<!-- Body -->
<div class="px-6 pb-6 pt-4 text-center">
<h5 class="font-semibold text-dark">
<a :href="action.route" class="hover:text-primary transition-colors">{{ title }}</a>
</h5>
<p class="mt-2 text-sm text-secondary leading-relaxed">{{ description }}</p>
<button
type="button"
class="mt-4 inline-flex items-center justify-center rounded-lg px-5 py-2 text-sm font-medium transition-all hover:-translate-y-0.5"
:class="btnClasses[action.color] ?? btnClasses.success"
>
{{ action.label }}
</button>
</div>
</div>
</template>
+47
View File
@@ -0,0 +1,47 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
title: { type: String, default: '' },
// Accepts HTML — e.g. "<span class='font-bold'>+15%</span> increase today"
subtitle: { type: String, default: '' },
// Timestamp label shown in footer — e.g. "updated 4 min ago"
update: { type: String, default: '' },
color: { type: String, default: 'primary' },
})
const gradientMap = {
primary: 'bg-gradient-primary shadow-primary',
secondary: 'bg-gradient-secondary shadow-secondary',
success: 'bg-gradient-success shadow-success',
warning: 'bg-gradient-warning shadow-warning',
danger: 'bg-gradient-danger shadow-danger',
info: 'bg-gradient-info shadow-info',
dark: 'bg-gradient-dark shadow-dark',
light: 'bg-gradient-light shadow-soft-sm',
}
const headerClass = computed(() => gradientMap[props.color] ?? gradientMap.primary)
</script>
<template>
<div class="rounded-2xl bg-white shadow-soft-md overflow-visible">
<!-- Floating chart header slot content sits inside the gradient area -->
<div class="relative -mt-6 mx-4 z-10 overflow-hidden rounded-xl px-3 py-3" :class="headerClass">
<slot />
</div>
<!-- Body -->
<div class="px-4 pt-3 pb-4">
<h6 class="mb-0 font-bold text-dark">{{ title }}</h6>
<p class="text-sm text-secondary mt-0.5" v-html="subtitle" />
<div class="my-3 border-t border-gray-100" />
<div class="flex items-center gap-1 text-sm text-secondary">
<span class="material-icons text-base leading-none">schedule</span>
{{ update }}
</div>
</div>
</div>
</template>
@@ -0,0 +1,48 @@
<script setup>
import { computed } from 'vue'
import { useCountUp } from '../../composables/useCountUp.js'
const props = defineProps({
count: { type: Number, required: true },
suffix: { type: String, default: '' },
duration: { type: Number, default: 2000 },
title: { type: String, required: true },
description: { type: String, required: true },
color: {
type: String,
default: 'success',
validator: v => ['primary','secondary','info','success','warning','danger','error','light','dark'].includes(v),
},
// 'horizontal' | 'vertical' | '' (none)
divider: { type: String, default: '' },
})
const { count } = useCountUp(props.count, props.duration)
// Gradient class for the count value — text-gradient clips the bg-gradient to text
const gradientMap = {
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 gradientClass = computed(() => gradientMap[props.color] ?? gradientMap.success)
</script>
<template>
<div class="text-center">
<h1 class="text-5xl font-black text-gradient" :class="gradientClass">
{{ count }}{{ suffix }}
</h1>
<h5 class="mt-3 font-semibold text-dark">{{ title }}</h5>
<p class="mt-1 text-sm text-secondary leading-relaxed">{{ description }}</p>
<div v-if="divider === 'horizontal'" class="mx-auto mt-4 h-px w-16 bg-gray-200" />
<div v-else-if="divider === 'vertical'" class="mx-auto mt-4 h-12 w-px bg-gray-200" />
</div>
</template>
@@ -0,0 +1,69 @@
<script setup>
import { computed } from 'vue'
// v-bind="$attrs" in the template passes attrs to the info wrapper, not the root
defineOptions({ inheritAttrs: false })
const props = defineProps({
// String icon name, or { component, color, size }
icon: { type: [String, Object], default: '' },
// { component (img src), class }
image: { type: Object, default: null },
// String, or { text, class }
title: { type: [String, Object], required: true },
// String, or { text, class }
description: { type: [String, Object], required: true },
})
const iconName = computed(() => typeof props.icon === 'object' ? props.icon.component : props.icon)
const iconColor = computed(() => typeof props.icon === 'object' ? `text-${props.icon.color}` : 'text-success')
const iconSize = computed(() => typeof props.icon === 'object' ? `text-${props.icon.size}` : 'text-3xl')
const titleText = computed(() => typeof props.title === 'string' ? props.title : props.title?.text ?? '')
const titleClass = computed(() => typeof props.title === 'object' ? props.title?.class ?? '' : 'text-lg font-bold text-dark')
const descText = computed(() => typeof props.description === 'string' ? props.description : props.description?.text ?? '')
const descClass = computed(() => typeof props.description === 'object' ? props.description?.class ?? '' : 'text-sm text-secondary')
// Icon text color — explicit map so Tailwind scanner finds these classes
const textColorMap = {
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
warning: 'text-warning',
danger: 'text-danger',
info: 'text-info',
dark: 'text-dark',
light: 'text-light',
white: 'text-white',
}
const resolvedIconColor = computed(() =>
typeof props.icon === 'object'
? (textColorMap[props.icon.color] ?? 'text-success')
: 'text-success'
)
</script>
<template>
<div v-bind="$attrs">
<!-- Icon -->
<span
v-if="iconName"
class="material-icons text-gradient"
:class="[resolvedIconColor, iconSize]"
>
{{ iconName }}
</span>
<!-- Image fallback -->
<img
v-else-if="image"
:src="image.component"
:class="image.class"
:alt="titleText"
/>
<h5 :class="['mt-3', titleClass]">{{ titleText }}</h5>
<p :class="['mt-1 leading-relaxed', descClass]">{{ descText }}</p>
</div>
</template>
@@ -0,0 +1,69 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
image: { type: String, default: '' },
color: { type: String, default: '' }, // color name: 'success', 'primary', etc. — empty = plain card
name: { type: String, required: true },
date: { type: String, required: true },
review: { type: String, required: true },
rating: { type: Number, required: true },
})
const cardMap = {
primary: 'bg-gradient-primary shadow-primary',
secondary: 'bg-gradient-secondary shadow-secondary',
success: 'bg-gradient-success shadow-success',
warning: 'bg-gradient-warning shadow-warning',
danger: 'bg-gradient-danger shadow-danger',
info: 'bg-gradient-info shadow-info',
dark: 'bg-gradient-dark shadow-dark',
}
const hasColor = computed(() => !!props.color)
const cardClass = computed(() => hasColor.value ? cardMap[props.color] ?? '' : 'bg-white shadow-soft-md')
const onDark = computed(() => hasColor.value)
// Build an array of star states: 'full', 'empty'
const stars = computed(() =>
Array.from({ length: 5 }, (_, i) => i < Math.round(props.rating) ? 'star' : 'star_border')
)
</script>
<template>
<div class="rounded-2xl p-6" :class="cardClass">
<!-- Floating avatar (overlaps top) -->
<img
v-if="image"
:src="image"
:alt="name"
class="h-14 w-14 rounded-xl object-cover shadow-soft-md -mt-10 mb-4"
/>
<!-- Author -->
<div class="flex items-center gap-2 mb-1">
<h6 class="font-bold" :class="onDark ? 'text-white' : 'text-dark'">{{ name }}</h6>
</div>
<p class="flex items-center gap-1 text-xs" :class="onDark ? 'text-white/70' : 'text-secondary'">
<span class="material-icons text-sm leading-none">schedule</span>
{{ date }}
</p>
<!-- Review text -->
<p class="mt-4 text-sm leading-relaxed" :class="onDark ? 'text-white/90' : 'text-secondary'">
"{{ review }}"
</p>
<!-- Star rating using Material Icons -->
<div class="mt-3 flex items-center gap-0.5">
<span
v-for="(star, i) in stars"
:key="i"
class="material-icons text-lg leading-none"
:class="onDark ? 'text-white' : 'text-warning'"
>
{{ star }}
</span>
</div>
</div>
</template>
+88
View File
@@ -0,0 +1,88 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
// { text: 'white'|'dark'|'', background: color name or full class }
color: {
type: Object,
default: () => ({ text: '', background: '' }),
},
// { component: material-icon-name, color: color-name }
icon: { type: Object, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
// { route, label: { text, color } }
action: { type: Object, required: true },
})
// Background: accepts a color name → gradient class, or passes through a full class string
const bgMap = {
primary: 'bg-gradient-primary',
secondary: 'bg-gradient-secondary',
success: 'bg-gradient-success',
warning: 'bg-gradient-warning',
danger: 'bg-gradient-danger',
info: 'bg-gradient-info',
dark: 'bg-gradient-dark',
light: 'bg-light',
white: 'bg-white',
}
const bgClass = computed(() => bgMap[props.color.background] ?? props.color.background ?? '')
const textMap = {
white: 'text-white',
dark: 'text-dark',
'': 'text-dark',
}
const textClass = computed(() => textMap[props.color.text] ?? 'text-dark')
const iconColorMap = {
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
warning: 'text-warning',
danger: 'text-danger',
info: 'text-info',
dark: 'text-dark',
white: 'text-white',
}
const iconClass = computed(() => iconColorMap[props.icon.color] ?? 'text-success')
const linkColorMap = {
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
warning: 'text-warning',
danger: 'text-danger',
info: 'text-info',
dark: 'text-dark',
white: 'text-white',
}
const linkClass = computed(() => linkColorMap[props.action.label?.color] ?? 'text-success')
</script>
<template>
<div
class="flex flex-col gap-4 rounded-2xl p-6 md:flex-row md:items-start"
:class="bgClass"
>
<!-- Icon -->
<span class="material-icons shrink-0 text-3xl" :class="iconClass">
{{ icon.component }}
</span>
<!-- Text -->
<div class="flex-1">
<h5 class="font-bold" :class="textClass">{{ title }}</h5>
<p class="mt-1 text-sm leading-relaxed" :class="textClass">{{ description }}</p>
<a
:href="action.route"
class="mt-3 inline-flex items-center gap-1 text-sm font-medium transition-opacity hover:opacity-80"
:class="linkClass"
>
{{ action.label.text }}
<span class="material-icons text-base leading-none">arrow_forward</span>
</a>
</div>
</div>
</template>
@@ -0,0 +1,55 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
image: { type: String, required: true },
// { name, link }
profile: { type: Object, required: true },
// { label, color }
position: { type: Object, required: true },
description: { type: String, required: true },
})
const positionColorMap = {
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
warning: 'text-warning',
danger: 'text-danger',
info: 'text-info',
dark: 'text-dark',
light: 'text-secondary',
}
const positionClass = computed(() => positionColorMap[props.position.color] ?? 'text-secondary')
</script>
<template>
<div class="overflow-hidden rounded-2xl bg-white shadow-soft-md">
<div class="flex flex-col sm:flex-row">
<!-- Image column overlaps the card top on mobile -->
<div class="sm:w-2/5 shrink-0">
<a :href="profile.link ?? '#'">
<img
:src="image"
:alt="profile.name"
class="h-48 w-full object-cover sm:h-full sm:rounded-l-2xl"
/>
</a>
</div>
<!-- Bio column -->
<div class="flex flex-1 flex-col justify-center p-6">
<h5 class="font-bold text-dark">
<a :href="profile.link ?? '#'" class="hover:text-primary transition-colors">
{{ profile.name }}
</a>
</h5>
<h6 class="mt-0.5 text-sm font-medium" :class="positionClass">
{{ position.label }}
</h6>
<p class="mt-3 text-sm text-secondary leading-relaxed">{{ description }}</p>
</div>
</div>
</div>
</template>
@@ -0,0 +1,61 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
// { text: 'Label', value: '$53k' }
title: { type: Object, required: true },
// HTML string — e.g. "<span class='text-success'>+55%</span> than last week"
detail: { type: String, default: '' },
// { name: 'weekend', color: 'white'|color-name, background: color-name }
icon: {
type: Object,
default: () => ({ name: 'bar_chart', color: 'white', background: 'dark' }),
},
// Swap icon to the right, value to the left
directionReverse: { type: Boolean, default: false },
})
const gradientMap = {
primary: 'bg-gradient-primary shadow-primary',
secondary: 'bg-gradient-secondary shadow-secondary',
success: 'bg-gradient-success shadow-success',
warning: 'bg-gradient-warning shadow-warning',
danger: 'bg-gradient-danger shadow-danger',
info: 'bg-gradient-info shadow-info',
dark: 'bg-gradient-dark shadow-dark',
light: 'bg-gradient-light shadow-soft-sm',
}
const iconClass = computed(() => gradientMap[props.icon.background] ?? gradientMap.dark)
const iconTextClass = computed(() =>
props.icon.color === 'white' ? 'text-white' : `text-${props.icon.color}`
)
</script>
<template>
<div class="rounded-2xl bg-white shadow-soft-md">
<!-- Header: floating icon + metric -->
<div class="relative px-4 pt-2 pb-3">
<div
class="absolute -top-5 flex h-14 w-14 items-center justify-center rounded-xl transition-all"
:class="[iconClass, directionReverse ? 'right-4' : 'left-4']"
>
<span class="material-icons text-2xl opacity-90" :class="iconTextClass">
{{ icon.name }}
</span>
</div>
<div class="pt-1" :class="directionReverse ? 'text-left pl-16' : 'text-right'">
<p class="mb-0 text-sm text-secondary capitalize">{{ title.text }}</p>
<h4 class="mb-0 text-xl font-bold text-dark">{{ title.value }}</h4>
</div>
</div>
<div class="mx-4 border-t border-gray-100" />
<!-- Footer: detail text -->
<div class="px-4 py-3">
<p class="mb-0 text-sm text-secondary" v-html="detail" />
</div>
</div>
</template>
+35
View File
@@ -0,0 +1,35 @@
<script setup>
const props = defineProps({
color: { type: String, default: 'success' },
minHeight: { type: String, default: '26rem' },
})
// Colored shadow on the flip card — safelisted in style.css @source inline()
const shadowMap = {
primary: 'shadow-primary',
secondary: 'shadow-secondary',
success: 'shadow-success',
warning: 'shadow-warning',
danger: 'shadow-danger',
info: 'shadow-info',
dark: 'shadow-dark',
}
</script>
<template>
<div class="rotating-card-container">
<!--
card-rotate is defined in style.css @layer utilities:
transform-style: preserve-3d
transition: transform 0.6s
On parent hover rotateY(180deg)
-->
<div
class="card-rotate relative rounded-2xl"
:class="shadowMap[color] ?? shadowMap.success"
:style="{ minHeight: minHeight }"
>
<slot />
</div>
</div>
</template>
@@ -0,0 +1,60 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
image: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
// Array of { route, label, color }
action: { type: Array, required: true },
})
const btnMap = {
white: 'bg-white text-dark hover:bg-gray-100',
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',
}
function btnClass(color) {
return btnMap[color] ?? btnMap.white
}
</script>
<template>
<!--
.back is defined in style.css:
backface-visibility: hidden
position: absolute; inset: 0
transform: rotateY(180deg) pre-rotated; shows when parent flips
-->
<div
class="back overflow-hidden rounded-2xl bg-cover bg-center"
:style="{ backgroundImage: `url(${image})` }"
>
<div class="absolute inset-0 rounded-2xl bg-black/60" />
<div class="relative flex h-full flex-col items-center justify-center px-8 py-10 text-center">
<h3 class="text-2xl font-bold text-white leading-snug" v-html="title" />
<p class="mt-3 text-sm text-white/80 leading-relaxed">{{ description }}</p>
<div class="mt-6 flex flex-wrap items-center justify-center gap-2">
<a
v-for="(btn, i) in action"
:key="i"
:href="btn.route"
target="_blank"
rel="noopener"
class="rounded-lg px-5 py-2 text-sm font-medium transition-all hover:-translate-y-0.5"
:class="btnClass(btn.color)"
>
{{ btn.label }}
</a>
</div>
</div>
</div>
</template>
@@ -0,0 +1,33 @@
<script setup>
defineProps({
image: { type: String, required: true },
icon: { type: String, default: '' },
label: { type: String, default: '' },
title: { type: String, required: true },
description: { type: String, required: true },
})
</script>
<template>
<!--
.front is defined in style.css:
backface-visibility: hidden
position: absolute; inset: 0
-->
<div
class="front overflow-hidden rounded-2xl bg-cover bg-center"
:style="{ backgroundImage: `url(${image})` }"
>
<!-- Dark overlay for text legibility -->
<div class="absolute inset-0 rounded-2xl bg-black/50" />
<div class="relative flex h-full flex-col items-center justify-center px-8 py-10 text-center">
<span v-if="icon" class="material-icons mb-3 text-5xl text-white">{{ icon }}</span>
<p v-if="label" class="mb-2 text-xs font-medium uppercase tracking-widest text-white/70">
{{ label }}
</p>
<h3 class="text-2xl font-bold text-white leading-snug" v-html="title" />
<p class="mt-3 text-sm text-white/80 leading-relaxed">{{ description }}</p>
</div>
</div>
</template>
+64
View File
@@ -0,0 +1,64 @@
<script setup>
import { inject, computed } from 'vue'
const props = defineProps({
// Material icon name, e.g. 'notifications'
icon: { type: String, default: 'circle' },
// Design-system color name for the icon dot
color: { type: String, default: 'success' },
title: { type: String, required: true },
dateTime: { type: String, default: '' },
description: { type: String, default: '' },
// Pass true on the last item to hide the connector line
last: { type: Boolean, default: false },
})
const dark = inject('mk-timeline-dark', false)
// Explicit map — avoids dynamic `bg-${color}/10` which Tailwind can't scan
const dotMap = {
primary: 'bg-primary/10 text-primary',
secondary: 'bg-secondary/10 text-secondary',
success: 'bg-success/10 text-success',
warning: 'bg-warning/10 text-warning',
danger: 'bg-danger/10 text-danger',
info: 'bg-info/10 text-info',
dark: 'bg-dark/10 text-dark',
}
const dotClass = computed(() => dotMap[props.color] ?? dotMap.success)
</script>
<template>
<div class="flex gap-4" :class="last ? '' : 'mb-1'">
<!-- Icon dot + vertical connector -->
<div class="flex flex-col items-center">
<div
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
:class="dotClass"
>
<span class="material-icons text-base leading-none">{{ icon }}</span>
</div>
<!-- Connector line hidden on last item -->
<div v-if="!last" class="mt-1 w-px flex-1 bg-gray-200" />
</div>
<!-- Content -->
<div class="pb-4" :class="last ? 'pb-1' : ''">
<h6
class="mb-0 text-sm font-bold leading-snug"
:class="dark ? 'text-white' : 'text-dark'"
>
{{ title }}
</h6>
<p class="mt-0.5 mb-0 text-xs text-secondary">{{ dateTime }}</p>
<p
v-if="description"
class="mt-2 mb-0 text-sm"
:class="dark ? 'text-white/80' : 'text-secondary'"
>
{{ description }}
</p>
</div>
</div>
</template>
+30
View File
@@ -0,0 +1,30 @@
<script setup>
import { provide } from 'vue'
const props = defineProps({
title: { type: String, default: '' },
description: { type: String, default: '' },
dark: { type: Boolean, default: false },
})
// MkTimelineItem reads this to adapt its text colors
provide('mk-timeline-dark', props.dark)
</script>
<template>
<div
class="rounded-2xl shadow-soft-md h-full"
:class="dark ? 'bg-gradient-dark' : 'bg-white'"
>
<!-- Header -->
<div class="px-4 pt-4 pb-0">
<h6 class="font-bold" :class="dark ? 'text-white' : 'text-dark'">{{ title }}</h6>
<p class="text-sm text-secondary" v-html="description" />
</div>
<!-- Timeline items -->
<div class="px-4 pt-2 pb-4">
<slot />
</div>
</div>
</template>
@@ -0,0 +1,56 @@
<script setup>
import { computed } from 'vue'
defineProps({
image: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
action: {
type: Object,
default: () => ({ route: '#', color: 'success', label: 'Read more' }),
},
})
const linkColors = {
primary: 'text-primary',
secondary: 'text-secondary',
success: 'text-success',
warning: 'text-warning',
danger: 'text-danger',
info: 'text-info',
dark: 'text-dark',
white: 'text-white',
}
</script>
<template>
<div class="flex flex-col">
<!-- Image no card wrapper, just soft shadow and rounded corners -->
<div class="overflow-hidden rounded-xl shadow-soft-md">
<a :href="action.route" class="block">
<img
:src="image"
:alt="title"
loading="lazy"
class="h-48 w-full object-cover transition-transform duration-500 hover:scale-105"
/>
</a>
</div>
<!-- Body no background, plain text -->
<div class="mt-4">
<h5 class="font-bold text-dark">
<a :href="action.route" class="hover:text-primary transition-colors">{{ title }}</a>
</h5>
<p class="mt-2 text-sm text-secondary leading-relaxed">{{ description }}</p>
<a
:href="action.route"
class="mt-3 inline-flex items-center gap-1 text-sm font-medium transition-opacity hover:opacity-80"
:class="linkColors[action.color] ?? linkColors.success"
>
{{ action.label }}
<span class="material-icons text-base leading-none">arrow_forward</span>
</a>
</div>
</div>
</template>
+48
View File
@@ -0,0 +1,48 @@
<script setup>
defineProps({
// [{ route: '/path', label: 'Label' }]
routes: {
type: Array,
required: true,
},
})
</script>
<template>
<nav aria-label="breadcrumb">
<ol class="flex flex-wrap items-center gap-1.5 text-sm">
<li
v-for="({ route, label }, index) in routes"
:key="index"
class="flex items-center gap-1.5"
>
<!-- Separator (skip for first item) -->
<span
v-if="index > 0"
class="material-icons text-base leading-none text-gray-400"
aria-hidden="true"
>
chevron_right
</span>
<!-- Link (all except last) -->
<a
v-if="index < routes.length - 1"
:href="route"
class="font-medium text-secondary no-underline transition-colors hover:text-dark"
>
{{ label }}
</a>
<!-- Current page (last item) -->
<span
v-else
class="font-medium text-dark"
aria-current="page"
>
{{ label }}
</span>
</li>
</ol>
</nav>
</template>
+96
View File
@@ -0,0 +1,96 @@
<script setup>
defineProps({
brand: {
type: Object,
default: () => ({ name: 'Brand', logo: '', route: '/' }),
},
// [{ icon: '<i class="fab fa-...">' or material-icon-name, link, label? }]
socials: {
type: Array,
default: () => [],
},
// [{ name: 'Column Title', items: [{ name, href }] }]
menus: {
type: Array,
default: () => [],
},
copyright: {
type: String,
default: '',
},
})
const year = new Date().getFullYear()
// Detect HTML-string icons vs material icon names
function isHtml(str) {
return typeof str === 'string' && str.trimStart().startsWith('<')
}
</script>
<template>
<footer class="border-t border-gray-100 bg-white pt-12 pb-8">
<div class="mx-auto max-w-7xl px-6">
<div class="grid gap-8 lg:grid-cols-5">
<!-- Brand column -->
<div class="lg:col-span-1">
<a :href="brand.route" class="inline-block">
<img v-if="brand.logo" :src="brand.logo" :alt="brand.name" class="mb-3 h-8 w-auto" />
<span v-else class="text-base font-bold text-dark">{{ brand.name }}</span>
</a>
<p v-if="!brand.logo" class="mt-2 text-xs text-secondary">{{ brand.name }}</p>
<!-- Socials -->
<div v-if="socials.length" class="mt-4 flex items-center gap-3">
<a
v-for="s in socials"
:key="s.link"
:href="s.link"
:aria-label="s.label"
target="_blank"
rel="noopener"
class="flex h-8 w-8 items-center justify-center rounded-lg text-secondary transition-colors hover:bg-gray-100 hover:text-dark"
>
<!-- HTML icon string (Font Awesome etc.) -->
<span v-if="isHtml(s.icon)" v-html="s.icon" />
<!-- Material icon name -->
<span v-else class="material-icons text-base leading-none">{{ s.icon }}</span>
</a>
</div>
</div>
<!-- Menu columns -->
<div
v-for="menu in menus"
:key="menu.name"
class="lg:col-span-1"
>
<h6 class="mb-3 text-xs font-bold uppercase tracking-wider text-dark">
{{ menu.name }}
</h6>
<ul class="space-y-2 list-none p-0">
<li v-for="item in menu.items" :key="item.name">
<a
:href="item.href"
target="_blank"
rel="noopener"
class="text-sm text-secondary no-underline transition-colors hover:text-dark"
>
{{ item.name }}
</a>
</li>
</ul>
</div>
</div>
<!-- Copyright -->
<div class="mt-10 border-t border-gray-100 pt-6 text-center">
<p class="text-xs text-secondary">
{{ copyright || `Copyright © ${year} — All rights reserved.` }}
</p>
</div>
</div>
</footer>
</template>
@@ -0,0 +1,67 @@
<script setup>
defineProps({
// [{ name, href }]
links: {
type: Array,
default: () => [],
},
// [{ icon: HTML-string or material-icon-name, link, label? }]
socials: {
type: Array,
default: () => [],
},
copyright: {
type: String,
default: '',
},
})
const year = new Date().getFullYear()
function isHtml(str) {
return typeof str === 'string' && str.trimStart().startsWith('<')
}
</script>
<template>
<footer class="border-t border-gray-100 bg-white py-10">
<div class="mx-auto max-w-7xl px-6">
<!-- Nav links -->
<div v-if="links.length" class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
<a
v-for="link in links"
:key="link.name"
:href="link.href"
target="_blank"
rel="noopener"
class="text-sm text-secondary no-underline transition-colors hover:text-dark"
>
{{ link.name }}
</a>
</div>
<!-- Social icons -->
<div v-if="socials.length" class="mt-6 flex items-center justify-center gap-4">
<a
v-for="s in socials"
:key="s.link"
:href="s.link"
:aria-label="s.label"
target="_blank"
rel="noopener"
class="flex h-9 w-9 items-center justify-center rounded-lg text-secondary transition-colors hover:bg-gray-100 hover:text-dark"
>
<span v-if="isHtml(s.icon)" v-html="s.icon" />
<span v-else class="material-icons text-lg leading-none">{{ s.icon }}</span>
</a>
</div>
<!-- Copyright -->
<p class="mt-6 text-center text-xs text-secondary">
{{ copyright || `Copyright © ${year} — All rights reserved.` }}
</p>
</div>
</footer>
</template>
+81
View File
@@ -0,0 +1,81 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
image: { type: String, default: '' },
// String, or { text, variant: 'h1''h6', class }
title: { type: [String, Object], default: '' },
// String, or { text, class }
description: { type: [String, Object], default: '' },
// Overlay color name, or full class string, or '' for no overlay
mask: { type: String, default: 'dark' },
// 0.01.0 overlay opacity
maskOpacity: { type: Number, default: 0.5 },
center: { type: Boolean, default: false },
minHeight: { type: String, default: '75vh' },
fullWidth: { type: Boolean, default: false },
})
const titleText = computed(() => typeof props.title === 'string' ? props.title : props.title?.text ?? '')
const titleTag = computed(() => typeof props.title === 'object' && props.title?.variant ? props.title.variant : 'h1')
const titleClass = computed(() => typeof props.title === 'object' ? props.title?.class ?? '' : '')
const descText = computed(() => typeof props.description === 'string' ? props.description : props.description?.text ?? '')
const descClass = computed(() => typeof props.description === 'object' ? props.description?.class ?? '' : '')
// Resolve mask to a gradient background class or custom class
const gradientMap = {
primary: 'bg-gradient-primary',
secondary: 'bg-gradient-secondary',
success: 'bg-gradient-success',
warning: 'bg-gradient-warning',
danger: 'bg-gradient-danger',
info: 'bg-gradient-info',
dark: 'bg-gradient-dark',
light: 'bg-gradient-light',
}
const maskClass = computed(() => gradientMap[props.mask] ?? props.mask ?? 'bg-gradient-dark')
</script>
<template>
<header>
<div
class="relative flex items-center overflow-hidden bg-cover bg-center"
:style="{ backgroundImage: image ? `url(${image})` : undefined, minHeight }"
>
<!-- Gradient mask overlay -->
<div
v-if="mask"
class="absolute inset-0"
:class="maskClass"
:style="{ opacity: maskOpacity }"
/>
<!-- Content -->
<div
class="relative z-10 w-full"
:class="fullWidth ? 'px-6' : 'mx-auto max-w-7xl px-6'"
>
<div :class="center ? 'mx-auto max-w-2xl text-center' : 'max-w-xl'">
<component
:is="titleTag"
class="font-bold text-white"
:class="[titleClass, center ? 'text-4xl lg:text-5xl' : 'text-3xl lg:text-4xl']"
>
{{ titleText }}
</component>
<p
v-if="descText"
class="mt-4 text-lg text-white/80 leading-relaxed"
:class="descClass"
>
{{ descText }}
</p>
<slot />
</div>
</div>
</div>
</header>
</template>
+251
View File
@@ -0,0 +1,251 @@
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
brand: {
type: Object,
default: () => ({ name: 'Brand', route: '/' }),
},
// [{ label, href?, icon?, children?: [{ label, href, description? }] }]
navItems: {
type: Array,
default: () => [],
},
// { label, href, color }
action: {
type: Object,
default: () => ({ label: 'Get Started', href: '#', color: 'primary' }),
},
transparent: { type: Boolean, default: false },
dark: { type: Boolean, default: false },
sticky: { type: Boolean, default: true },
})
// ── Scroll blur ───────────────────────────────────────────────
const scrolled = ref(false)
function onScroll() { scrolled.value = window.scrollY > 20 }
onMounted(() => window.addEventListener('scroll', onScroll, { passive: true }))
onUnmounted(() => window.removeEventListener('scroll', onScroll))
// ── Mobile menu ───────────────────────────────────────────────
const mobileOpen = ref(false)
const openDropdown = ref(null)
function toggleDropdown(label) {
openDropdown.value = openDropdown.value === label ? null : label
}
// Close dropdowns on outside click
function onDocClick(e) {
if (!e.target.closest('[data-nav-item]')) openDropdown.value = null
}
onMounted(() => document.addEventListener('click', onDocClick))
onUnmounted(() => document.removeEventListener('click', onDocClick))
// ── Computed styles ───────────────────────────────────────────
const isBlurred = computed(() => props.transparent && scrolled.value)
const navClass = computed(() => {
if (props.dark) return 'bg-gradient-dark shadow-dark'
if (props.transparent && !scrolled.value) return 'bg-transparent'
return 'bg-white/90 backdrop-blur-sm shadow-soft-sm'
})
const textClass = computed(() => {
if (props.dark) return 'text-white'
if (props.transparent && !scrolled.value) return 'text-white'
return 'text-dark'
})
const actionColors = {
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',
white: 'bg-white text-dark shadow-soft-sm',
}
const actionClass = computed(() => actionColors[props.action.color] ?? actionColors.primary)
</script>
<template>
<nav
class="top-0 z-30 w-full transition-all duration-300"
:class="[navClass, sticky ? 'sticky' : 'absolute']"
>
<div class="mx-auto max-w-7xl px-4">
<div class="flex h-16 items-center justify-between">
<!-- Brand -->
<a
:href="brand.route"
class="text-sm font-bold no-underline transition-opacity hover:opacity-80"
:class="textClass"
>
{{ brand.name }}
</a>
<!-- Desktop nav -->
<ul class="hidden items-center gap-1 list-none lg:flex">
<li
v-for="item in navItems"
:key="item.label"
class="relative"
data-nav-item
>
<!-- Item with children (dropdown) -->
<template v-if="item.children?.length">
<button
class="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors"
:class="[textClass, 'hover:bg-black/5']"
@click.stop="toggleDropdown(item.label)"
>
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
{{ item.label }}
<span class="material-icons text-sm leading-none opacity-60 transition-transform duration-200"
:class="openDropdown === item.label ? 'rotate-180' : ''">
expand_more
</span>
</button>
<!-- Dropdown panel -->
<Transition name="dropdown">
<div
v-if="openDropdown === item.label"
class="absolute left-0 top-full mt-2 w-52 rounded-xl bg-white p-2 shadow-soft-lg"
>
<template v-for="child in item.children" :key="child.label">
<a
:href="child.href"
class="flex flex-col rounded-lg px-3 py-2.5 text-sm text-dark no-underline transition-colors hover:bg-gray-50"
@click="openDropdown = null"
>
<span class="font-medium">{{ child.label }}</span>
<span v-if="child.description" class="mt-0.5 text-xs text-secondary">{{ child.description }}</span>
</a>
</template>
</div>
</Transition>
</template>
<!-- Plain link -->
<a
v-else
:href="item.href"
class="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium no-underline transition-colors"
:class="[textClass, 'hover:bg-black/5']"
>
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
{{ item.label }}
</a>
</li>
</ul>
<!-- CTA + mobile toggle -->
<div class="flex items-center gap-3">
<a
:href="action.href"
class="hidden rounded-lg px-5 py-2 text-sm font-medium no-underline transition-all hover:-translate-y-0.5 lg:inline-flex"
:class="actionClass"
>
{{ action.label }}
</a>
<!-- Hamburger -->
<button
class="flex h-9 w-9 items-center justify-center rounded-lg transition-colors hover:bg-black/5 lg:hidden"
:class="textClass"
@click="mobileOpen = !mobileOpen"
aria-label="Toggle menu"
>
<span class="material-icons">{{ mobileOpen ? 'close' : 'menu' }}</span>
</button>
</div>
</div>
</div>
<!-- Mobile menu -->
<Transition name="mobile-menu">
<div v-if="mobileOpen" class="border-t border-gray-100 bg-white lg:hidden">
<div class="mx-auto max-w-7xl px-4 py-3 space-y-1">
<template v-for="item in navItems" :key="item.label">
<!-- Mobile dropdown -->
<template v-if="item.children?.length">
<button
class="flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium text-dark hover:bg-gray-50"
@click="toggleDropdown(item.label)"
>
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
{{ item.label }}
<span class="material-icons ml-auto text-sm leading-none opacity-60 transition-transform duration-200"
:class="openDropdown === item.label ? 'rotate-180' : ''">expand_more</span>
</button>
<div v-if="openDropdown === item.label" class="ml-6 space-y-1">
<a
v-for="child in item.children"
:key="child.label"
:href="child.href"
class="block rounded-lg px-3 py-2 text-sm text-secondary no-underline hover:bg-gray-50 hover:text-dark"
@click="mobileOpen = false"
>
{{ child.label }}
</a>
</div>
</template>
<!-- Mobile plain link -->
<a
v-else
:href="item.href"
class="flex items-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium text-dark no-underline hover:bg-gray-50"
@click="mobileOpen = false"
>
<span v-if="item.icon" class="material-icons text-base leading-none opacity-60">{{ item.icon }}</span>
{{ item.label }}
</a>
</template>
<!-- Mobile CTA -->
<div class="pt-2 pb-1">
<a
:href="action.href"
class="flex w-full items-center justify-center rounded-lg px-5 py-2.5 text-sm font-medium no-underline"
:class="actionClass"
@click="mobileOpen = false"
>
{{ action.label }}
</a>
</div>
</div>
</div>
</Transition>
</nav>
</template>
<style scoped>
.dropdown-enter-active,
.dropdown-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-6px);
}
.mobile-menu-enter-active,
.mobile-menu-leave-active {
transition: opacity 0.2s ease, max-height 0.25s ease;
overflow: hidden;
max-height: 600px;
}
.mobile-menu-enter-from,
.mobile-menu-leave-to {
opacity: 0;
max-height: 0;
}
</style>
+30
View File
@@ -0,0 +1,30 @@
import { ref, onMounted, onUnmounted } from 'vue'
/**
* Animates a number from 0 to `endVal` over `duration` ms using
* easeOutQuart easing. Replaces the vue-count-to dependency.
*/
export function useCountUp(endVal, duration = 2000) {
const count = ref(0)
let rafId = null
onMounted(() => {
const startTime = performance.now()
function tick(now) {
const elapsed = now - startTime
const progress = Math.min(elapsed / duration, 1)
const eased = 1 - Math.pow(1 - progress, 4) // easeOutQuart
count.value = Math.round(eased * endVal)
if (progress < 1) rafId = requestAnimationFrame(tick)
}
rafId = requestAnimationFrame(tick)
})
onUnmounted(() => {
if (rafId !== null) cancelAnimationFrame(rafId)
})
return { count }
}
+94
View File
@@ -0,0 +1,94 @@
// ── Core UI ───────────────────────────────────────────────────
export { default as MkAlert } from './components/MkAlert.vue'
export { default as MkAvatar } from './components/MkAvatar.vue'
export { default as MkBadge } from './components/MkBadge.vue'
export { default as MkButton } from './components/MkButton.vue'
export { default as MkCheckbox } from './components/MkCheckbox.vue'
export { default as MkInput } from './components/MkInput.vue'
export { default as MkPagination } from './components/MkPagination.vue'
export { default as MkPaginationItem } from './components/MkPaginationItem.vue'
export { default as MkProgress } from './components/MkProgress.vue'
export { default as MkSocialButton } from './components/MkSocialButton.vue'
export { default as MkSwitch } from './components/MkSwitch.vue'
export { default as MkTable } from './components/MkTable.vue'
export { default as MkTextArea } from './components/MkTextArea.vue'
// ── Cards ─────────────────────────────────────────────────────
export { default as MkBackgroundBlogCard } from './components/cards/MkBackgroundBlogCard.vue'
export { default as MkCenteredBlogCard } from './components/cards/MkCenteredBlogCard.vue'
export { default as MkDefaultCounterCard } from './components/cards/MkDefaultCounterCard.vue'
export { default as MkDefaultInfoCard } from './components/cards/MkDefaultInfoCard.vue'
export { default as MkDefaultReviewCard } from './components/cards/MkDefaultReviewCard.vue'
export { default as MkFilledInfoCard } from './components/cards/MkFilledInfoCard.vue'
export { default as MkHorizontalTeamCard } from './components/cards/MkHorizontalTeamCard.vue'
export { default as MkRotatingCard } from './components/cards/MkRotatingCard.vue'
export { default as MkRotatingCardBack } from './components/cards/MkRotatingCardBack.vue'
export { default as MkRotatingCardFront } from './components/cards/MkRotatingCardFront.vue'
export { default as MkTransparentBlogCard } from './components/cards/MkTransparentBlogCard.vue'
export { default as MkMiniStatisticsCard } from './components/cards/MkMiniStatisticsCard.vue'
export { default as MkChartCard } from './components/cards/MkChartCard.vue'
export { default as MkTimelineList } from './components/cards/MkTimelineList.vue'
export { default as MkTimelineItem } from './components/cards/MkTimelineItem.vue'
// ── Layout ────────────────────────────────────────────────────
export { default as MkBreadcrumbs } from './components/layout/MkBreadcrumbs.vue'
export { default as MkFooter } from './components/layout/MkFooter.vue'
export { default as MkFooterCentered } from './components/layout/MkFooterCentered.vue'
export { default as MkHeader } from './components/layout/MkHeader.vue'
export { default as MkNavbar } from './components/layout/MkNavbar.vue'
// ── Composables ───────────────────────────────────────────────
export { useCountUp } from './composables/useCountUp.js'
// ── Vue plugin (global registration) ─────────────────────────
import MkAlert from './components/MkAlert.vue'
import MkAvatar from './components/MkAvatar.vue'
import MkBadge from './components/MkBadge.vue'
import MkButton from './components/MkButton.vue'
import MkCheckbox from './components/MkCheckbox.vue'
import MkInput from './components/MkInput.vue'
import MkPagination from './components/MkPagination.vue'
import MkPaginationItem from './components/MkPaginationItem.vue'
import MkProgress from './components/MkProgress.vue'
import MkSocialButton from './components/MkSocialButton.vue'
import MkSwitch from './components/MkSwitch.vue'
import MkTable from './components/MkTable.vue'
import MkTextArea from './components/MkTextArea.vue'
import MkBackgroundBlogCard from './components/cards/MkBackgroundBlogCard.vue'
import MkCenteredBlogCard from './components/cards/MkCenteredBlogCard.vue'
import MkDefaultCounterCard from './components/cards/MkDefaultCounterCard.vue'
import MkDefaultInfoCard from './components/cards/MkDefaultInfoCard.vue'
import MkDefaultReviewCard from './components/cards/MkDefaultReviewCard.vue'
import MkFilledInfoCard from './components/cards/MkFilledInfoCard.vue'
import MkHorizontalTeamCard from './components/cards/MkHorizontalTeamCard.vue'
import MkRotatingCard from './components/cards/MkRotatingCard.vue'
import MkRotatingCardBack from './components/cards/MkRotatingCardBack.vue'
import MkRotatingCardFront from './components/cards/MkRotatingCardFront.vue'
import MkTransparentBlogCard from './components/cards/MkTransparentBlogCard.vue'
import MkMiniStatisticsCard from './components/cards/MkMiniStatisticsCard.vue'
import MkChartCard from './components/cards/MkChartCard.vue'
import MkTimelineList from './components/cards/MkTimelineList.vue'
import MkTimelineItem from './components/cards/MkTimelineItem.vue'
import MkBreadcrumbs from './components/layout/MkBreadcrumbs.vue'
import MkFooter from './components/layout/MkFooter.vue'
import MkFooterCentered from './components/layout/MkFooterCentered.vue'
import MkHeader from './components/layout/MkHeader.vue'
import MkNavbar from './components/layout/MkNavbar.vue'
const components = [
MkAlert, MkAvatar, MkBadge, MkButton, MkCheckbox, MkInput,
MkPagination, MkPaginationItem, MkProgress, MkSocialButton,
MkSwitch, MkTable, MkTextArea,
MkBackgroundBlogCard, MkCenteredBlogCard, MkDefaultCounterCard,
MkDefaultInfoCard, MkDefaultReviewCard, MkFilledInfoCard,
MkHorizontalTeamCard, MkRotatingCard, MkRotatingCardBack,
MkRotatingCardFront, MkTransparentBlogCard,
MkMiniStatisticsCard, MkChartCard, MkTimelineList, MkTimelineItem,
MkBreadcrumbs, MkFooter, MkFooterCentered, MkHeader, MkNavbar,
]
export const MkDesignSystem = {
install(app) {
components.forEach(c => app.component(c.__name, c))
},
}
+1
View File
@@ -12,6 +12,7 @@ const navItems = [
{ label: 'Icons', path: '/icons', icon: 'auto_awesome' }, { label: 'Icons', path: '/icons', icon: 'auto_awesome' },
{ label: 'Maps', path: '/maps', icon: 'map' }, { label: 'Maps', path: '/maps', icon: 'map' },
{ label: 'Notifications', path: '/notifications',icon: 'notifications' }, { label: 'Notifications', path: '/notifications',icon: 'notifications' },
{ label: 'Showcase', path: '/showcase', icon: 'widgets' },
] ]
function isActive(path) { function isActive(path) {
+1
View File
@@ -11,5 +11,6 @@ export default createRouter({
{ path: '/maps', component: () => import('../views/MapsView.vue'), meta: { title: 'Maps' } }, { path: '/maps', component: () => import('../views/MapsView.vue'), meta: { title: 'Maps' } },
{ path: '/notifications',component: () => import('../views/NotificationsView.vue'), meta: { title: 'Notifications' } }, { path: '/notifications',component: () => import('../views/NotificationsView.vue'), meta: { title: 'Notifications' } },
{ path: '/profile', component: () => import('../views/ProfileView.vue'), meta: { title: 'Profile' } }, { path: '/profile', component: () => import('../views/ProfileView.vue'), meta: { title: 'Profile' } },
{ path: '/showcase', component: () => import('../views/ShowcaseView.vue'), meta: { title: 'Component Showcase' } },
], ],
}) })
-3
View File
@@ -23,9 +23,6 @@ const statusClass = {
cancelled: 'bg-danger/10 text-danger', cancelled: 'bg-danger/10 text-danger',
} }
const gradientProgress = (value, gradient) => ({
width: `${value}%`,
})
</script> </script>
<template> <template>
+197 -49
View File
@@ -1,6 +1,63 @@
<script setup> <script setup>
import { ref, onUnmounted } from 'vue'
import DashboardLayout from '../layouts/DashboardLayout.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 = [ const stats = [
{ label: 'Followers', value: '4,812' }, { label: 'Followers', value: '4,812' },
{ label: 'Following', value: '294' }, { label: 'Following', value: '294' },
@@ -18,7 +75,7 @@ const conversations = [
{ initials: 'JD', color: 'info', name: 'James Donovan', preview: 'Can you review my PR when you get a chance?', time: '22m' }, { 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: '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: '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: 'AM', color: 'secondary', name: 'Ally Maria', preview: 'Updated the font preload entries in index.html.',time: '1d' },
] ]
const projects = [ const projects = [
@@ -34,10 +91,27 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
<template> <template>
<DashboardLayout> <DashboardLayout>
<!-- Cover + avatar --> <!-- Hidden file inputs -->
<div class="relative mb-20"> <input ref="coverInputRef" type="file" accept="image/*" class="hidden" @change="onCoverPicked" />
<div class="h-48 overflow-hidden rounded-2xl bg-gradient-dark shadow-dark"> <input ref="avatarInputRef" type="file" accept="image/*" class="hidden" @change="onAvatarPicked" />
<!-- Decorative grid -->
<!-- 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"> <svg class="h-full w-full opacity-10" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<pattern id="pgrid" width="40" height="40" patternUnits="userSpaceOnUse"> <pattern id="pgrid" width="40" height="40" patternUnits="userSpaceOnUse">
@@ -46,25 +120,102 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
</defs> </defs>
<rect width="100%" height="100%" fill="url(#pgrid)" /> <rect width="100%" height="100%" fill="url(#pgrid)" />
</svg> </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> </div>
<!-- Avatar --> <!-- 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"> <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 EC
</div> </div>
</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"> <div class="grid gap-6 lg:grid-cols-3">
<!-- Left column --> <!-- Left column -->
<div class="space-y-6"> <div class="space-y-6">
<!-- Identity card -->
<div class="rounded-2xl bg-white p-6 shadow-soft-md"> <div class="rounded-2xl bg-white p-6 shadow-soft-md">
<h4 class="text-xl font-bold text-dark">Esthera Carter</h4> <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> <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 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"> <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> <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>
</div> </div>
<!-- Bio -->
<p class="mt-5 text-sm leading-relaxed text-secondary"> <p class="mt-5 text-sm leading-relaxed text-secondary">
Building design systems and component libraries. Obsessed with typography, Building design systems and component libraries. Obsessed with typography,
spacing, and making sure every pixel is intentional. spacing, and making sure every pixel is intentional.
</p> </p>
<!-- Socials -->
<div class="mt-5 flex items-center gap-3"> <div class="mt-5 flex items-center gap-3">
<a <a
v-for="s in socials" v-for="s in socials"
@@ -92,14 +241,13 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
</div> </div>
</div> </div>
<!-- Conversations -->
<div class="rounded-2xl bg-white p-6 shadow-soft-md"> <div class="rounded-2xl bg-white p-6 shadow-soft-md">
<h6 class="mb-4 text-sm font-bold text-dark">Conversations</h6> <h6 class="mb-4 text-sm font-bold text-dark">Conversations</h6>
<div class="space-y-4"> <div class="space-y-4">
<div <div
v-for="c in conversations" v-for="c in conversations"
:key="c.name" :key="c.name"
class="flex items-start gap-3 cursor-pointer group" class="group flex cursor-pointer items-start gap-3"
> >
<div <div
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl text-xs font-bold text-white" 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 }} {{ c.initials }}
</div> </div>
<div class="flex-1 min-w-0"> <div class="min-w-0 flex-1">
<p class="text-sm font-medium text-dark group-hover:text-primary transition-colors">{{ c.name }}</p> <p class="text-sm font-medium text-dark transition-colors group-hover:text-primary">{{ c.name }}</p>
<p class="text-xs text-secondary truncate">{{ c.preview }}</p> <p class="truncate text-xs text-secondary">{{ c.preview }}</p>
</div> </div>
<span class="shrink-0 text-xs text-secondary">{{ c.time }}</span> <span class="shrink-0 text-xs text-secondary">{{ c.time }}</span>
</div> </div>
@@ -121,7 +269,6 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
<!-- Right column --> <!-- Right column -->
<div class="space-y-6 lg:col-span-2"> <div class="space-y-6 lg:col-span-2">
<!-- Projects -->
<div class="rounded-2xl bg-white p-6 shadow-soft-md"> <div class="rounded-2xl bg-white p-6 shadow-soft-md">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h6 class="text-sm font-bold text-dark">Projects</h6> <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" :key="p.name"
class="group relative overflow-hidden rounded-xl border border-gray-100 p-4 transition-shadow hover:shadow-soft-md" 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="absolute left-0 top-0 h-full w-1 rounded-l-xl" :class="`bg-gradient-${p.gradient}`" />
<div class="pl-3"> <div class="pl-3">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
@@ -154,10 +299,7 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
<span class="material-icons text-base leading-none">more_vert</span> <span class="material-icons text-base leading-none">more_vert</span>
</button> </button>
</div> </div>
<div class="mt-3 flex -space-x-2">
<!-- Member avatars -->
<div class="mt-3 flex items-center">
<div class="flex -space-x-2">
<div <div
v-for="m in p.members" v-for="m in p.members"
:key="m" :key="m"
@@ -172,9 +314,7 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Settings -->
<div class="rounded-2xl bg-white p-6 shadow-soft-md"> <div class="rounded-2xl bg-white p-6 shadow-soft-md">
<h6 class="mb-5 text-sm font-bold text-dark">Profile Settings</h6> <h6 class="mb-5 text-sm font-bold text-dark">Profile Settings</h6>
<div class="space-y-4"> <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 class="grid gap-4 sm:grid-cols-2">
<div> <div>
<label class="mb-1 block text-xs font-medium text-secondary">First Name</label> <label class="mb-1 block text-xs font-medium text-secondary">First Name</label>
<input <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" />
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"
/>
</div> </div>
<div> <div>
<label class="mb-1 block text-xs font-medium text-secondary">Last Name</label> <label class="mb-1 block text-xs font-medium text-secondary">Last Name</label>
<input <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" />
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"
/>
</div> </div>
</div> </div>
<div> <div>
<label class="mb-1 block text-xs font-medium text-secondary">Email Address</label> <label class="mb-1 block text-xs font-medium text-secondary">Email Address</label>
<input <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" />
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"
/>
</div> </div>
<div> <div>
<label class="mb-1 block text-xs font-medium text-secondary">Location</label> <label class="mb-1 block text-xs font-medium text-secondary">Location</label>
<input <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" />
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"
/>
</div> </div>
<div> <div>
<label class="mb-1 block text-xs font-medium text-secondary">Bio</label> <label class="mb-1 block text-xs font-medium text-secondary">Bio</label>
<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>
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>
</div> </div>
<div class="flex justify-end"> <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 hover:-translate-y-0.5 active:translate-y-0 transition-all">
Save Changes Save Changes
</button> </button>
</div> </div>
@@ -238,3 +359,30 @@ const memberColor = { PC: 'primary', JD: 'info', SR: 'success', ML: 'warning', A
</DashboardLayout> </DashboardLayout>
</template> </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>
+579
View File
@@ -0,0 +1,579 @@
<script setup>
import { ref } from 'vue'
import DashboardLayout from '../layouts/DashboardLayout.vue'
// Core
import MkButton from '../components/MkButton.vue'
import MkBadge from '../components/MkBadge.vue'
import MkAlert from '../components/MkAlert.vue'
import MkAvatar from '../components/MkAvatar.vue'
import MkProgress from '../components/MkProgress.vue'
import MkCheckbox from '../components/MkCheckbox.vue'
import MkSwitch from '../components/MkSwitch.vue'
import MkInput from '../components/MkInput.vue'
import MkTextArea from '../components/MkTextArea.vue'
import MkPagination from '../components/MkPagination.vue'
import MkPaginationItem from '../components/MkPaginationItem.vue'
import MkTable from '../components/MkTable.vue'
// Cards
import MkBackgroundBlogCard from '../components/cards/MkBackgroundBlogCard.vue'
import MkCenteredBlogCard from '../components/cards/MkCenteredBlogCard.vue'
import MkTransparentBlogCard from '../components/cards/MkTransparentBlogCard.vue'
import MkDefaultInfoCard from '../components/cards/MkDefaultInfoCard.vue'
import MkFilledInfoCard from '../components/cards/MkFilledInfoCard.vue'
import MkDefaultCounterCard from '../components/cards/MkDefaultCounterCard.vue'
import MkDefaultReviewCard from '../components/cards/MkDefaultReviewCard.vue'
import MkHorizontalTeamCard from '../components/cards/MkHorizontalTeamCard.vue'
import MkRotatingCard from '../components/cards/MkRotatingCard.vue'
import MkRotatingCardFront from '../components/cards/MkRotatingCardFront.vue'
import MkRotatingCardBack from '../components/cards/MkRotatingCardBack.vue'
import MkMiniStatisticsCard from '../components/cards/MkMiniStatisticsCard.vue'
import MkChartCard from '../components/cards/MkChartCard.vue'
import MkTimelineList from '../components/cards/MkTimelineList.vue'
import MkTimelineItem from '../components/cards/MkTimelineItem.vue'
// Layout
import MkBreadcrumbs from '../components/layout/MkBreadcrumbs.vue'
import MkNavbar from '../components/layout/MkNavbar.vue'
import MkHeader from '../components/layout/MkHeader.vue'
import MkFooterCentered from '../components/layout/MkFooterCentered.vue'
// ── Reactive state ────────────────────────────────────────────
const checkbox1 = ref(true)
const checkbox2 = ref(false)
const switch1 = ref(true)
const switch2 = ref(false)
const inputVal = ref('')
const textareaVal = ref('')
const activePage = ref(3)
// ── Data ──────────────────────────────────────────────────────
const colors = ['primary','secondary','success','warning','danger','info','dark']
const img = (seed, w = 600, h = 400) => `https://picsum.photos/seed/${seed}/${w}/${h}`
const tableRows = [
{ initials:'EC', color:'primary', name:'Esthera Carter', email:'e.carter@mk.dev', position:['Lead Designer','Creative'], status:true, date:'23/04/18', action:{label:'Edit', route:'#'} },
{ initials:'JD', color:'info', name:'James Donovan', email:'j.donovan@mk.dev',position:['Developer','Engineering'], status:true, date:'11/01/19', action:{label:'Edit', route:'#'} },
{ initials:'SR', color:'success', name:'Sofia Reyes', email:'s.reyes@mk.dev', position:['Product Manager','Operations'], status:false, date:'19/09/20', action:{label:'Edit', route:'#'} },
{ initials:'ML', color:'warning', name:'Marco Lin', email:'m.lin@mk.dev', position:['QA Engineer','Quality'], status:true, date:'24/12/21', action:{label:'Edit', route:'#'} },
{ initials:'AM', color:'secondary', name:'Ally Maria', email:'a.maria@mk.dev', position:['UX Researcher','Design'], status:false, date:'04/10/22', action:{label:'Edit', route:'#'} },
]
const navItems = [
{ label:'Home', href:'#', icon:'home' },
{ label:'Components', icon:'widgets', children:[
{ label:'Buttons', href:'#', description:'Variants, sizes and states' },
{ label:'Cards', href:'#', description:'Blog, info, counter, review' },
{ label:'Forms', href:'#', description:'Inputs, selects, checkboxes' },
]},
{ label:'Docs', href:'#', icon:'article' },
]
const breadcrumbRoutes = [
{ route:'/', label:'Dashboard' },
{ route:'/showcase', label:'Components' },
{ route:'#', label:'Showcase' },
]
</script>
<template>
<DashboardLayout>
<div class="space-y-12">
<!-- SECTION HELPER -->
<!-- Each section follows: title / subtitle / content card -->
<!-- Buttons -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Buttons</h2>
<p class="text-sm text-secondary mb-4">variant × color × size</p>
<div class="rounded-2xl bg-white p-6 shadow-soft-md space-y-6">
<!-- Gradient -->
<div>
<p class="mb-3 text-xs font-medium uppercase tracking-wide text-secondary">Gradient</p>
<div class="flex flex-wrap gap-3">
<MkButton v-for="c in colors" :key="c" variant="gradient" :color="c">{{ c }}</MkButton>
</div>
</div>
<!-- Contained -->
<div>
<p class="mb-3 text-xs font-medium uppercase tracking-wide text-secondary">Contained</p>
<div class="flex flex-wrap gap-3">
<MkButton v-for="c in colors" :key="c" variant="contained" :color="c">{{ c }}</MkButton>
</div>
</div>
<!-- Outline -->
<div>
<p class="mb-3 text-xs font-medium uppercase tracking-wide text-secondary">Outline</p>
<div class="flex flex-wrap gap-3">
<MkButton v-for="c in colors" :key="c" variant="outline" :color="c">{{ c }}</MkButton>
</div>
</div>
<!-- Sizes + states -->
<div>
<p class="mb-3 text-xs font-medium uppercase tracking-wide text-secondary">Sizes & states</p>
<div class="flex flex-wrap items-center gap-3">
<MkButton variant="gradient" color="primary" size="sm">Small</MkButton>
<MkButton variant="gradient" color="primary" size="md">Medium</MkButton>
<MkButton variant="gradient" color="primary" size="lg">Large</MkButton>
<MkButton variant="gradient" color="primary" :disabled="true">Disabled</MkButton>
<MkButton variant="outline" color="primary" :full-width="true" class="max-w-xs">Full width</MkButton>
</div>
</div>
</div>
</section>
<!-- Badges -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Badges</h2>
<p class="text-sm text-secondary mb-4">fill · gradient · sizes · rounded</p>
<div class="rounded-2xl bg-white p-6 shadow-soft-md space-y-4">
<div class="flex flex-wrap gap-2">
<MkBadge v-for="c in colors" :key="c" variant="fill" :color="c">{{ c }}</MkBadge>
</div>
<div class="flex flex-wrap gap-2">
<MkBadge v-for="c in colors" :key="c" variant="gradient" :color="c">{{ c }}</MkBadge>
</div>
<div class="flex flex-wrap items-center gap-2">
<MkBadge color="primary" size="sm" :rounded="true">sm pill</MkBadge>
<MkBadge color="primary" size="md" :rounded="true">md pill</MkBadge>
<MkBadge color="primary" size="lg" :rounded="true">lg pill</MkBadge>
</div>
</div>
</section>
<!-- Alerts -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Alerts</h2>
<p class="text-sm text-secondary mb-4">all colors · dismissible</p>
<div class="rounded-2xl bg-white p-6 shadow-soft-md space-y-3">
<MkAlert color="success">Success action completed.</MkAlert>
<MkAlert color="info">Info something you should know.</MkAlert>
<MkAlert color="warning">Warning check before proceeding.</MkAlert>
<MkAlert color="danger" :dismissible="true">Danger (dismissible) click × to close.</MkAlert>
<MkAlert color="dark">Dark a neutral message.</MkAlert>
</div>
</section>
<!-- Avatars -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Avatars</h2>
<p class="text-sm text-secondary mb-4">xxs xxl · borderRadius variants</p>
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
<div class="flex flex-wrap items-end gap-4">
<div v-for="s in ['xxs','xs','sm','md','lg','xl','xxl']" :key="s" class="flex flex-col items-center gap-2">
<MkAvatar :image="img('av1',200,200)" alt="Avatar" :size="s" />
<span class="text-xs text-secondary">{{ s }}</span>
</div>
<div class="flex flex-col items-center gap-2">
<MkAvatar :image="img('av2',200,200)" alt="Circle" size="lg" border-radius="full" />
<span class="text-xs text-secondary">circle</span>
</div>
</div>
</div>
</section>
<!-- Progress -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Progress</h2>
<p class="text-sm text-secondary mb-4">contained · gradient</p>
<div class="rounded-2xl bg-white p-6 shadow-soft-md space-y-3">
<div v-for="c in colors" :key="c" class="flex items-center gap-4">
<span class="w-20 shrink-0 text-xs text-secondary">{{ c }}</span>
<div class="flex-1"><MkProgress variant="gradient" :color="c" :value="Math.round(30 + colors.indexOf(c) * 10)" /></div>
</div>
</div>
</section>
<!-- Form Controls -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Form Controls</h2>
<p class="text-sm text-secondary mb-4">input · textarea · checkbox · switch</p>
<div class="grid gap-6 lg:grid-cols-2">
<!-- Inputs -->
<div class="rounded-2xl bg-white p-6 shadow-soft-md space-y-4">
<MkInput label="Default input" v-model="inputVal" placeholder="Type something…" />
<MkInput label="With icon" icon="search" v-model="inputVal" placeholder="Search…" />
<MkInput label="Error state" v-model="inputVal" :error="true" placeholder="Required field" />
<MkInput label="Success state" v-model="inputVal" :success="true" placeholder="Looks good" />
<MkInput label="Disabled" v-model="inputVal" :is-disabled="true" placeholder="Not editable" />
<MkTextArea v-model="textareaVal" placeholder="Your message…" :rows="3">Message</MkTextArea>
</div>
<!-- Toggles -->
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
<p class="mb-4 text-xs font-medium uppercase tracking-wide text-secondary">Checkboxes</p>
<div class="space-y-3 mb-6">
<MkCheckbox id="c1" v-model="checkbox1" color="primary">Primary (checked)</MkCheckbox>
<MkCheckbox id="c2" v-model="checkbox2" color="success">Success (unchecked)</MkCheckbox>
<MkCheckbox id="c3" v-model="checkbox1" color="danger">Danger</MkCheckbox>
</div>
<p class="mb-4 text-xs font-medium uppercase tracking-wide text-secondary">Switches</p>
<div class="space-y-3">
<MkSwitch id="s1" v-model="switch1" color="primary">Primary (on)</MkSwitch>
<MkSwitch id="s2" v-model="switch2" color="success">Success (off)</MkSwitch>
<MkSwitch id="s3" v-model="switch1" color="info">Info</MkSwitch>
</div>
</div>
</div>
</section>
<!-- Pagination -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Pagination</h2>
<p class="text-sm text-secondary mb-4">color variants · active state</p>
<div class="rounded-2xl bg-white p-6 shadow-soft-md space-y-4">
<div v-for="c in ['primary','success','info','danger']" :key="c">
<MkPagination :color="c">
<MkPaginationItem :prev="true" @click="activePage > 1 && activePage--" />
<MkPaginationItem v-for="n in 5" :key="n" :label="String(n)" :active="activePage === n" @click="activePage = n" />
<MkPaginationItem :next="true" @click="activePage < 5 && activePage++" />
</MkPagination>
</div>
</div>
</section>
<!-- Blog Cards -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Blog Cards</h2>
<p class="text-sm text-secondary mb-4">background · centered · transparent</p>
<div class="grid gap-6 md:grid-cols-3">
<MkBackgroundBlogCard
:image="img('bg1')"
title="Background Blog Card"
description="Full-bleed image with gradient overlay. Content sits above the image."
:action="{ route:'#', label:'Read more', color:'white' }"
/>
<MkCenteredBlogCard
:image="img('cb1')"
title="Centered Blog Card"
description="Floating image header overlaps the card top edge for a layered effect."
:action="{ color:'success', label:'Find Out More', route:'#' }"
/>
<MkTransparentBlogCard
:image="img('tb1')"
title="Transparent Blog Card"
description="No card background — just the image with a soft shadow and plain text below."
:action="{ route:'#', color:'primary', label:'Read more' }"
/>
</div>
</section>
<!-- Info Cards -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Info Cards</h2>
<p class="text-sm text-secondary mb-4">default · filled</p>
<div class="grid gap-6 md:grid-cols-2">
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
<div class="grid gap-6 sm:grid-cols-2">
<MkDefaultInfoCard
v-for="item in [
{icon:{component:'bolt',color:'primary',size:'3xl'},title:'Fast Build',description:'Zero-config Vite setup with Tailwind v4.'},
{icon:{component:'palette',color:'success',size:'3xl'},title:'Design Tokens',description:'All values live in @theme CSS custom properties.'},
{icon:{component:'widgets',color:'info',size:'3xl'},title:'30+ Components',description:'Buttons, cards, forms, navbars and more.'},
{icon:{component:'devices',color:'warning',size:'3xl'},title:'Responsive',description:'Mobile-first layouts using Tailwind breakpoints.'},
]"
:key="item.title"
:icon="item.icon"
:title="item.title"
:description="item.description"
/>
</div>
</div>
<div class="space-y-4">
<MkFilledInfoCard
:color="{ background:'primary', text:'white' }"
:icon="{ component:'rocket_launch', color:'white' }"
title="Ship faster"
description="Pre-built components mean less time on UI and more time on features."
:action="{ route:'#', label:{ text:'Learn More', color:'white' } }"
/>
<MkFilledInfoCard
:color="{ background:'', text:'' }"
:icon="{ component:'verified', color:'success' }"
title="Design conformity"
description="The conformity checklist helps keep every PR on-brand."
:action="{ route:'#', label:{ text:'View Checklist', color:'success' } }"
/>
</div>
</div>
</section>
<!-- ⑩ Dashboard Cards ───────────────────────────────────── -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Dashboard Cards</h2>
<p class="text-sm text-secondary mb-4">mini statistics · chart holder · timeline</p>
<!-- Mini statistics row (matches Material Dashboard layout) -->
<div class="grid gap-6 sm:grid-cols-2 xl:grid-cols-4 mb-8 mt-8">
<MkMiniStatisticsCard
:title="{ text: 'Today\'s Money', value: '$53k' }"
detail="<span class='text-success font-medium'>+55%</span> than last week"
:icon="{ name: 'weekend', color: 'white', background: 'primary' }"
/>
<MkMiniStatisticsCard
:title="{ text: 'Today\'s Users', value: '2,300' }"
detail="<span class='text-success font-medium'>+3%</span> than last month"
:icon="{ name: 'leaderboard', color: 'white', background: 'info' }"
/>
<MkMiniStatisticsCard
:title="{ text: 'New Clients', value: '3,462' }"
detail="<span class='text-danger font-medium'>-2%</span> than yesterday"
:icon="{ name: 'person', color: 'white', background: 'success' }"
/>
<MkMiniStatisticsCard
:title="{ text: 'Sales', value: '$103k' }"
detail="<span class='text-success font-medium'>+5%</span> just updated"
:icon="{ name: 'store', color: 'white', background: 'warning' }"
/>
</div>
<!-- Chart cards row -->
<div class="grid gap-6 md:grid-cols-3 mb-8 mt-12">
<!-- Bar chart placeholder -->
<MkChartCard
title="Website Views"
subtitle="Last campaign performance"
update="campaign sent 2 days ago"
color="primary"
>
<svg viewBox="0 0 140 60" class="w-full h-20" preserveAspectRatio="none">
<rect x="8" y="30" width="12" height="30" rx="2" fill="white" opacity="0.7"/>
<rect x="28" y="42" width="12" height="18" rx="2" fill="white" opacity="0.7"/>
<rect x="48" y="20" width="12" height="40" rx="2" fill="white" opacity="0.7"/>
<rect x="68" y="10" width="12" height="50" rx="2" fill="white" opacity="0.7"/>
<rect x="88" y="35" width="12" height="25" rx="2" fill="white" opacity="0.7"/>
<rect x="108" y="22" width="12" height="38" rx="2" fill="white" opacity="0.7"/>
<rect x="128" y="15" width="12" height="45" rx="2" fill="white" opacity="0.7"/>
</svg>
</MkChartCard>
<!-- Line chart placeholder -->
<MkChartCard
title="Daily Sales"
subtitle="<span class='font-bold'>+15%</span> increase in today's sales"
update="updated 4 min ago"
color="success"
>
<svg viewBox="0 0 140 60" class="w-full h-20" preserveAspectRatio="none">
<polyline
points="0,50 18,38 35,42 52,20 70,25 88,12 105,18 122,8 140,14"
fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"
/>
<polyline
points="0,50 18,38 35,42 52,20 70,25 88,12 105,18 122,8 140,14 140,60 0,60"
fill="white" opacity="0.15"
/>
</svg>
</MkChartCard>
<!-- Line chart placeholder dark -->
<MkChartCard
title="Completed Tasks"
subtitle="Last campaign performance"
update="just updated"
color="dark"
>
<svg viewBox="0 0 140 60" class="w-full h-20" preserveAspectRatio="none">
<polyline
points="0,45 18,36 35,48 52,22 70,32 88,18 105,28 122,10 140,20"
fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity="0.9"
/>
<polyline
points="0,45 18,36 35,48 52,22 70,32 88,18 105,28 122,10 140,20 140,60 0,60"
fill="white" opacity="0.15"
/>
</svg>
</MkChartCard>
</div>
<!-- Timeline card -->
<div class="grid gap-6 md:grid-cols-2">
<MkTimelineList
title="Orders Overview"
description="<span class='text-success font-medium'>+24%</span> this month"
>
<MkTimelineItem icon="notifications" color="success" title="$2,400 Design changes" date-time="22 DEC 7:20 PM" />
<MkTimelineItem icon="code" color="danger" title="New order #1832412" date-time="21 DEC 11:00 PM" />
<MkTimelineItem icon="shopping_cart" color="info" title="Server payments for April" date-time="21 DEC 9:34 PM" />
<MkTimelineItem icon="credit_card" color="warning" title="New card added for order #4395133" date-time="20 DEC 2:20 AM" />
<MkTimelineItem icon="vpn_key" color="primary" title="Unlock packages for development" date-time="18 DEC 4:54 AM" :last="true" />
</MkTimelineList>
<!-- Dark variant -->
<MkTimelineList
title="Recent Activity"
description="<span class='text-white font-medium'>12 events</span> this week"
:dark="true"
>
<MkTimelineItem icon="check_circle" color="success" title="Design system shipped" date-time="Today 10:00 AM" />
<MkTimelineItem icon="build" color="info" title="Phase 4 components merged" date-time="Yesterday 3:40 PM" />
<MkTimelineItem icon="bug_report" color="danger" title="Alert transition bug fixed" date-time="2 days ago" />
<MkTimelineItem icon="star" color="warning" title="v0.1.0 tagged" date-time="3 days ago" :last="true" />
</MkTimelineList>
</div>
</section>
<!-- ⑪ Counter · Review · Team ───────────────────────────── -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Counter, Review & Team Cards</h2>
<p class="text-sm text-secondary mb-4">animated counters · star ratings · horizontal profile</p>
<div class="grid gap-6 lg:grid-cols-3">
<!-- Counter cards -->
<div class="rounded-2xl bg-white p-8 shadow-soft-md">
<div class="grid grid-cols-2 gap-6">
<MkDefaultCounterCard :count="240" suffix="+" color="primary" title="Projects" description="Completed this year" divider="horizontal" />
<MkDefaultCounterCard :count="4500" suffix="+" color="success" title="Users" description="Active accounts" divider="horizontal" />
<MkDefaultCounterCard :count="99" suffix="%" color="info" title="Uptime" description="Last 12 months" divider="horizontal" />
<MkDefaultCounterCard :count="18" color="warning" title="Countries" description="Worldwide reach" divider="horizontal" />
</div>
</div>
<!-- Review card -->
<MkDefaultReviewCard
:image="img('rev1',200,200)"
name="Sofia Reyes"
date="2 days ago"
review="This design system saved us weeks of setup. The components are clean and the token system makes theming trivial."
:rating="5"
/>
<!-- Team card -->
<MkHorizontalTeamCard
:image="img('team1',400,500)"
:profile="{ name:'James Donovan', link:'#' }"
:position="{ label:'Lead Engineer', color:'primary' }"
description="James leads the frontend platform team and maintains the component library. Open source contributor and design systems enthusiast."
/>
</div>
</section>
<!-- ⑪ Rotating Card ─────────────────────────────────────── -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Rotating Card</h2>
<p class="text-sm text-secondary mb-4">hover to flip — CSS 3D, no JS</p>
<div class="max-w-xs">
<div style="height: 22rem">
<MkRotatingCard color="primary" min-height="22rem">
<MkRotatingCardFront
:image="img('rot-front',600,800)"
icon="layers"
label="Design System"
title="Material Kit 2"
description="Vue 3 + Tailwind v4"
/>
<MkRotatingCardBack
:image="img('rot-back',600,800)"
title="Get Started"
description="Build beautiful interfaces with a consistent, token-driven system."
:action="[
{ route:'#', label:'Docs', color:'white' },
{ route:'#', label:'GitHub', color:'white' },
]"
/>
</MkRotatingCard>
</div>
</div>
</section>
<!-- ⑫ Table ──────────────────────────────────────────────── -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Table</h2>
<p class="text-sm text-secondary mb-4">avatar + status badge + action</p>
<MkTable :rows="tableRows">
<template #header>
<div class="flex items-center justify-between">
<h6 class="text-sm font-bold text-dark">Authors</h6>
<MkBadge color="success" :rounded="true">5 active</MkBadge>
</div>
</template>
<template #footer>
<MkPagination color="primary" size="sm">
<MkPaginationItem :prev="true" />
<MkPaginationItem label="1" :active="true" />
<MkPaginationItem label="2" />
<MkPaginationItem label="3" />
<MkPaginationItem :next="true" />
</MkPagination>
</template>
</MkTable>
</section>
<!-- ⑬ Layout Components ──────────────────────────────────── -->
<section>
<h2 class="text-lg font-bold text-dark mb-0.5">Layout Components</h2>
<p class="text-sm text-secondary mb-4">navbar · header · breadcrumbs · footer</p>
<div class="space-y-6">
<!-- Breadcrumbs -->
<div class="rounded-2xl bg-white p-6 shadow-soft-md">
<p class="mb-3 text-xs font-medium uppercase tracking-wide text-secondary">MkBreadcrumbs</p>
<MkBreadcrumbs :routes="breadcrumbRoutes" />
</div>
<!-- Navbar preview -->
<div class="rounded-2xl shadow-soft-md overflow-hidden">
<p class="bg-gray-50 px-4 py-2 text-xs font-medium text-secondary border-b border-gray-100">MkNavbar — light mode (hover items for dropdown)</p>
<MkNavbar
:brand="{ name:'MK Design', route:'#' }"
:nav-items="navItems"
:action="{ label:'Get Started', href:'#', color:'primary' }"
:sticky="false"
/>
</div>
<!-- Header preview -->
<div class="rounded-2xl shadow-soft-md overflow-hidden">
<p class="bg-gray-50 px-4 py-2 text-xs font-medium text-secondary border-b border-gray-100">MkHeader — background image + gradient mask</p>
<MkHeader
:image="img('header1',1200,600)"
:title="{ text:'Build Something Beautiful', variant:'h2' }"
description="A design system for Vue 3 and Tailwind v4."
mask="dark"
:mask-opacity="0.6"
:center="true"
min-height="200px"
/>
</div>
<!-- Footer preview -->
<div class="rounded-2xl shadow-soft-md overflow-hidden">
<p class="bg-gray-50 px-4 py-2 text-xs font-medium text-secondary border-b border-gray-100">MkFooterCentered</p>
<MkFooterCentered
:links="[
{ name:'Company', href:'#' },
{ name:'About', href:'#' },
{ name:'Blog', href:'#' },
{ name:'License', href:'#' },
]"
:socials="[
{ icon:'language', link:'#', label:'Website' },
{ icon:'code', link:'#', label:'GitHub' },
{ icon:'mail', link:'#', label:'Email' },
]"
:copyright="`© ${new Date().getFullYear()} MK Design System`"
/>
</div>
</div>
</section>
</div>
</DashboardLayout>
</template>
+1 -1
View File
@@ -17,7 +17,7 @@ const rows = [
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between border-b border-gray-100 px-6 py-4"> <div class="flex items-center justify-between border-b border-gray-100 px-6 py-4">
<h6 class="text-sm font-bold text-dark">Authors Table</h6> <h6 class="text-sm font-bold text-dark">Authors Table</h6>
<button class="rounded-lg bg-gradient-primary px-4 py-2 text-xs font-medium text-white shadow-primary hover:shadow-soft-md transition-shadow"> <button class="rounded-lg bg-gradient-primary px-4 py-2 text-xs font-medium text-white shadow-primary hover:-translate-y-0.5 active:translate-y-0 transition-all">
+ New Entry + New Entry
</button> </button>
</div> </div>
+121
View File
@@ -0,0 +1,121 @@
{
"meta": {
"project": "mk-design-system",
"sourceApproach": "tailwindcss-v4",
"primaryFont": "Roboto",
"fontType": "variable-woff2",
"colorMode": "light",
"version": "0.1.0"
},
"colors": {
"primary": { "DEFAULT": "#e91e63", "gradientStart": "#EC407A", "gradientEnd": "#D81B60" },
"secondary": { "DEFAULT": "#7b809a", "gradientStart": "#747b8a", "gradientEnd": "#495361" },
"semantic": {
"success": { "DEFAULT": "#4caf50", "gradientStart": "#66bb6a", "gradientEnd": "#43a047" },
"warning": { "DEFAULT": "#fb8c00", "gradientStart": "#ffa726", "gradientEnd": "#fb8c00" },
"danger": { "DEFAULT": "#f44335", "gradientStart": "#ef5350", "gradientEnd": "#e53935" },
"info": { "DEFAULT": "#1a73e8", "gradientStart": "#49a3f1", "gradientEnd": "#1a73e8" }
},
"neutrals": {
"white": "#ffffff",
"black": "#000000",
"light": "#f0f2f5",
"dark": "#344767",
"gray": {
"100": "#f8f9fa",
"200": "#f0f2f5",
"300": "#dee2e6",
"500": "#adb5bd",
"600": "#6c757d",
"900": "#212529"
}
}
},
"typography": {
"fontFamilies": {
"sans": "Roboto, Helvetica, Arial, sans-serif",
"serif": "Roboto Slab, serif",
"mono": "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace"
},
"fontFiles": {
"roboto": "public/fonts/roboto/Roboto-VariableFont_wdth-wght.woff2",
"robotoItalic":"public/fonts/roboto/Roboto-Italic-VariableFont_wdth-wght.woff2",
"robotoSlab": "public/fonts/roboto-slab/RobotoSlab-VariableFont_wght.woff2",
"materialIcons":"public/fonts/material-icons-round/material-icons-round-latin-400-normal.woff2"
},
"weightRange": "100900 (variable font)",
"sizes": {
"xs": "0.75rem",
"sm": "0.875rem",
"base": "1rem",
"lg": "1.125rem",
"xl": "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
"4xl": "2.25rem",
"5xl": "3rem"
},
"lineHeights": { "base": "1.5", "tight": "1.25", "relaxed": "1.75" }
},
"spacing": {
"note": "Tailwind v4 default scale — rem-based, no custom overrides needed",
"common": {
"1": "0.25rem", "2": "0.5rem", "3": "0.75rem",
"4": "1rem", "6": "1.5rem", "8": "2rem",
"12": "3rem", "16": "4rem", "24": "6rem"
}
},
"shadows": {
"soft": {
"xs": "0 2px 9px -5px rgba(0,0,0,0.15)",
"sm": "0 0.3125rem 0.625rem 0 rgba(0,0,0,0.12)",
"md": "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06)",
"lg": "0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05)"
},
"blur": "0 20px 27px rgba(0,0,0,0.05)",
"colored": {
"note": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba({r},{g},{b},0.4)",
"primary": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba(233,30,99,0.4)",
"secondary": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba(210,210,210,0.4)",
"success": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba(76,175,80,0.4)",
"warning": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba(251,140,0,0.4)",
"danger": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba(244,67,54,0.4)",
"info": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba(26,115,232,0.4)",
"dark": "0 4px 20px 0 rgba(0,0,0,0.14), 0 7px 10px -5px rgba(64,64,64,0.4)"
}
},
"borders": {
"radius": {
"xs": "0.1rem",
"sm": "0.125rem",
"md": "0.375rem",
"lg": "0.5rem",
"xl": "0.75rem",
"2xl": "1rem",
"full":"9999px"
},
"width": { "default": "1px" }
},
"gradients": {
"angle": "195deg",
"note": "All gradients use the 195deg angle — do not change",
"variants": {
"primary": "linear-gradient(195deg, #EC407A, #D81B60)",
"secondary": "linear-gradient(195deg, #747b8a, #495361)",
"success": "linear-gradient(195deg, #66bb6a, #43a047)",
"warning": "linear-gradient(195deg, #ffa726, #fb8c00)",
"danger": "linear-gradient(195deg, #ef5350, #e53935)",
"info": "linear-gradient(195deg, #49a3f1, #1a73e8)",
"dark": "linear-gradient(195deg, #42424a, #191919)",
"light": "linear-gradient(195deg, #ebeff4, #ced4da)"
}
},
"breakpoints": {
"note": "Tailwind v4 defaults",
"sm": "640px",
"md": "768px",
"lg": "1024px",
"xl": "1280px",
"2xl": "1536px"
}
}
+24 -2
View File
@@ -1,10 +1,32 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'path'
const isLib = process.env.BUILD_TARGET === 'lib'
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tailwindcss(), // Tailwind only for the app build — consuming projects bring their own
!isLib && tailwindcss(),
vue(), vue(),
], ].filter(Boolean),
build: isLib
? {
lib: {
entry: resolve(__dirname, 'src/index.js'),
name: 'MkDesignSystem',
fileName: format => `mk-design-system.${format}.js`,
formats: ['es', 'cjs'],
},
rollupOptions: {
// Vue must be provided by the consuming project
external: ['vue'],
output: { globals: { vue: 'Vue' } },
},
// Emit a separate CSS file (design-system.css) consumers can import
cssCodeSplit: false,
}
: {},
}) })