Initial Commit
This commit is contained in:
commit
1990111156
72 changed files with 3192 additions and 0 deletions
8
.editorconfig
Normal file
8
.editorconfig
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
74
.forgejo/workflows/ci.yml
Normal file
74
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-ui:
|
||||||
|
runs-on: self-hosted
|
||||||
|
container: git.mcpeakdev.com/mcpeakdev/bun-ci:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Audit dependencies
|
||||||
|
run: bun audit || true
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: bun run type-check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: bun run build-only
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output.zip
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [build-ui]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: self-hosted
|
||||||
|
container: git.mcpeakdev.com/mcpeakdev/docker-pub:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.mcpeakdev.com
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.BUN_AUTH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract image tag
|
||||||
|
id: meta
|
||||||
|
run: echo "tag=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: https://code.forgejo.org/forgejo/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-output.zip
|
||||||
|
path: dist
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.mcpeakdev.com/mcpeakdev/portfolio:${{ steps.meta.outputs.tag }}
|
||||||
|
git.mcpeakdev.com/mcpeakdev/portfolio:latest
|
||||||
|
cache-from: type=registry,ref=git.mcpeakdev.com/mcpeakdev/portfolio:latest
|
||||||
|
cache-to: type=inline
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
* text=auto eol=lf
|
||||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Cypress
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Vitest
|
||||||
|
__screenshots__/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
*.timestamp-*-*.mjs
|
||||||
5
.oxfmtrc.json
Normal file
5
.oxfmtrc.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
10
.oxlintrc.json
Normal file
10
.oxlintrc.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
|
||||||
|
"env": {
|
||||||
|
"browser": true
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"correctness": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"oxc.oxc-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
RUN rm -rf /usr/share/nginx/html/* /etc/nginx/conf.d/default.conf && \
|
||||||
|
mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \
|
||||||
|
/var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \
|
||||||
|
/var/cache/nginx/scgi_temp && \
|
||||||
|
chown -R 101:101 /var/cache/nginx /etc/nginx/conf.d && \
|
||||||
|
sed -i -e '/^user /d' -e 's|^pid .*|pid /tmp/nginx.pid;|' /etc/nginx/nginx.conf && \
|
||||||
|
printf '%s\n' \
|
||||||
|
'server {' \
|
||||||
|
' listen 8080;' \
|
||||||
|
' root /usr/share/nginx/html;' \
|
||||||
|
' index index.html;' \
|
||||||
|
' server_tokens off;' \
|
||||||
|
' gzip on;' \
|
||||||
|
' gzip_types text/css application/javascript application/json image/svg+xml;' \
|
||||||
|
' location = /healthz { access_log off; return 200 "ok\n"; }' \
|
||||||
|
' location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; }' \
|
||||||
|
' location = /index.html { add_header Cache-Control "no-cache"; }' \
|
||||||
|
' location / { try_files $uri $uri/ /index.html; }' \
|
||||||
|
'}' > /etc/nginx/conf.d/site.conf
|
||||||
|
|
||||||
|
COPY dist/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
USER 101
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD wget -qO- http://127.0.0.1:8080/healthz || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
24
components.json
Normal file
24
components.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"font": "inter",
|
||||||
|
"typescript": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/assets/index.css",
|
||||||
|
"baseColor": "zinc",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"pointer": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"composables": "@/composables"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
1
env.d.ts
vendored
Normal file
1
env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
26
eslint.config.ts
Normal file
26
eslint.config.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { globalIgnores } from 'eslint/config'
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||||
|
import skipFormatting from 'eslint-config-prettier/flat'
|
||||||
|
|
||||||
|
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||||
|
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||||
|
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||||
|
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{vue,ts,mts,tsx}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
vueTsConfigs.recommended,
|
||||||
|
|
||||||
|
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
||||||
|
|
||||||
|
skipFormatting,
|
||||||
|
)
|
||||||
23
index.html
Normal file
23
index.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Matthew McPeak - Full-Stack Engineer</title>
|
||||||
|
<meta name="description" content="Full-stack engineer in Kingsport, TN. Six years building SaaS for county clerks across Tennessee. Vue, TypeScript, Go.">
|
||||||
|
<meta property="og:title" content="Matthew McPeak - Full-Stack Engineer">
|
||||||
|
<meta property="og:description" content="Full-stack engineer in Kingsport, TN. Vue, TypeScript, Go.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://portfolio.mcpeakdev.com/">
|
||||||
|
<meta property="og:image" content="https://api.mcpeakdev.com/api/cdn/selfie.jpg">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="google-site-verification" content="wF4Cb06PaQIgplh5RwvS07BHxcOHDCShokuEoIzTm6o" />
|
||||||
|
<link rel="canonical" href="https://portfolio.mcpeakdev.com/">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
package.json
Normal file
54
package.json
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"name": "portfolio",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build",
|
||||||
|
"lint": "run-s \"lint:*\"",
|
||||||
|
"lint:oxlint": "oxlint . --fix",
|
||||||
|
"lint:eslint": "eslint . --fix --cache",
|
||||||
|
"format": "oxfmt src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@lucide/vue": "^1.52.0",
|
||||||
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
|
"@vueuse/core": "^15.0.0",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"reka-ui": "^2.11.0",
|
||||||
|
"shadcn-vue": "^2.8.2",
|
||||||
|
"tailwind-merge": "^3.7.0",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"vue": "^3.5.42",
|
||||||
|
"vue-router": "^5.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node24": "^24.0.5",
|
||||||
|
"@types/node": "^26.6.4",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.8",
|
||||||
|
"@vue/eslint-config-typescript": "^14.9.0",
|
||||||
|
"@vue/tsconfig": "^0.9.1",
|
||||||
|
"eslint": "^10.10.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-oxlint": "~1.82.0",
|
||||||
|
"eslint-plugin-vue": "~10.11.0",
|
||||||
|
"jiti": "^2.7.0",
|
||||||
|
"npm-run-all2": "^9.0.3",
|
||||||
|
"oxfmt": "^0.67.0",
|
||||||
|
"oxlint": "~1.82.0",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "~6.0.0",
|
||||||
|
"vite": "^8.2.2",
|
||||||
|
"vite-plugin-vue-devtools": "^8.2.1",
|
||||||
|
"vue-eslint-parser": "^10.4.1",
|
||||||
|
"vue-tsc": "^3.3.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.18.0 || >=24.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
4
public/robots.txt
Normal file
4
public/robots.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://portfolio.mcpeakdev.com/sitemap.xml
|
||||||
35
src/App.vue
Normal file
35
src/App.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import AppFooter from '@/components/AppFooter.vue';
|
||||||
|
import AppHeader from '@/components/AppHeader.vue';
|
||||||
|
import SocialLinks from './components/SocialLinks.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-auto md:h-screen scroll-smooth">
|
||||||
|
<a href="#main"
|
||||||
|
class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 focus:z-50 focus:bg-background focus:text-foreground focus:px-4 focus:py-2 focus:rounded-md focus:outline-2">
|
||||||
|
Skip to content
|
||||||
|
</a>
|
||||||
|
<header class="sticky top-0 z-50">
|
||||||
|
<AppHeader />
|
||||||
|
</header>
|
||||||
|
<main class="px-4 py-4 flex-grow" >
|
||||||
|
<div class="flex flex-col md:flex-row gap-10 justify-center h-full">
|
||||||
|
<div class="flex flex-col md:flex-row place-self-center">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<img class="sepia-70 h-96 w-auto aspect-[1.21] object-cover rounded-md " src="https://api.mcpeakdev.com/api/cdn/selfie.jpg" alt="Matthew McPeak"/>
|
||||||
|
<SocialLinks />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 justify-center basis-8/12 px-2 md:px-0 ">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="md:sticky md:bottom-0" >
|
||||||
|
<AppFooter />
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
140
src/assets/index.css
Normal file
140
src/assets/index.css
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn-vue/tailwind.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-heading: var(--font-sans);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--primary: oklch(0.2708 0.1111 29.23);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.2708 0.1111 29.23);
|
||||||
|
--muted: oklch(0.967 0.001 286.375);
|
||||||
|
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||||
|
--accent: oklch(0.2708 0.1111 29.23);
|
||||||
|
--accent-foreground: oklch(0.967 0.001 286.375);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.92 0.004 286.32);
|
||||||
|
--input: oklch(0.92 0.004 286.32);
|
||||||
|
--ring: oklch(0.705 0.015 286.067);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||||
|
--sidebar-primary: oklch(0.2708 0.1111 29.23);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2708 0.1111 29.23);
|
||||||
|
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||||
|
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.141 0.005 285.823);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.141 0.005 285.823);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.21 0.006 285.885);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.2708 0.1111 29.23);
|
||||||
|
--primary-foreground:oklch(0.92 0.004 286.32);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.274 0.006 286.033);
|
||||||
|
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||||
|
--accent: oklch(0.2708 0.1111 29.23);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.552 0.016 285.938);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.21 0.006 285.885);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.2708 0.1111 29.23);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.2708 0.1111 29.23);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg[role="img"], svg[role="header"] {
|
||||||
|
fill: var(--foreground)
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus > svg[role="img"], *:hover > svg[role="img"] {
|
||||||
|
fill: var(--primary-foreground)
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50 transition-colors duration-300;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
a, button {
|
||||||
|
@apply hover:!bg-accent focus:!bg-accent focus:!text-primary-foreground hover:!text-primary-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.animate-in, .animate-out { animation: none !important; transition: none !important; }
|
||||||
|
}
|
||||||
27
src/components/AppFooter.vue
Normal file
27
src/components/AppFooter.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
import SocialLinks from './SocialLinks.vue';
|
||||||
|
|
||||||
|
import { CopyrightIcon } from '@lucide/vue'
|
||||||
|
import ThemeToggleBtn from './ThemeToggleBtn.vue';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="rounded-none py-3 z-50">
|
||||||
|
<CardContent class="flex justify-center md:justify-between flex-row animate-in slide-in-from-bottom duration-500">
|
||||||
|
<div class="flex p-2 justify-center text-muted-foreground w-full md:w-auto">
|
||||||
|
<CopyrightIcon :size="15" class="mt-1 mr-1"/>
|
||||||
|
<p class="text-sm">Matthew McPeak</p>
|
||||||
|
</div>
|
||||||
|
<div class="justify-end basis-3/12 hidden md:flex">
|
||||||
|
<ThemeToggleBtn />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
33
src/components/AppHeader.vue
Normal file
33
src/components/AppHeader.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, type Component } from "vue"
|
||||||
|
import { useColorMode } from "@vueuse/core"
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
import NavSheet from "@/components/NavSheet.vue"
|
||||||
|
import Nav from "@/components/Nav.vue"
|
||||||
|
import TitleName from "@/components/TitleName.vue"
|
||||||
|
|
||||||
|
import ThemeToggleBtn from "@/components/ThemeToggleBtn.vue"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="rounded-none py-3 z-50">
|
||||||
|
<CardContent class="flex justify-between px-3 animate-in slide-in-from-top duration-500">
|
||||||
|
<div class="flex gap-3 justify-start ">
|
||||||
|
<NavSheet class="md:hidden"/>
|
||||||
|
<TitleName />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 justify-end ">
|
||||||
|
<Nav aria-label="Main" class="hidden md:inline" />
|
||||||
|
<div class="md:hidden flex">
|
||||||
|
<ThemeToggleBtn />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
63
src/components/Nav.vue
Normal file
63
src/components/Nav.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { RouterLink, useRoute } from "vue-router";
|
||||||
|
|
||||||
|
import Button from "@/components/ui/button/Button.vue";
|
||||||
|
import { Routes } from "@/types/routes";
|
||||||
|
|
||||||
|
import { Home, Pencil, BriefcaseBusiness} from '@lucide/vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
orientation?: "horizontal" | "vertical"
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentRoute = useRoute()
|
||||||
|
|
||||||
|
const { orientation = "horizontal" } = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{"route:clicked": []}>()
|
||||||
|
|
||||||
|
const isVertical = computed(() => orientation === "horizontal" ? false : true);
|
||||||
|
|
||||||
|
const routes = ref( [
|
||||||
|
{
|
||||||
|
icon: Home,
|
||||||
|
title: "Home",
|
||||||
|
url: Routes.Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Pencil,
|
||||||
|
title: "Skills",
|
||||||
|
url: Routes.Skills
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BriefcaseBusiness,
|
||||||
|
title: "Work",
|
||||||
|
url: Routes.Work
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleNavDrawer = () => {
|
||||||
|
emit("route:clicked");
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav>
|
||||||
|
<div class="flex gap-3 items-start" :class="{ 'flex-col': isVertical }">
|
||||||
|
<template v-for="route in routes" :key="route.title">
|
||||||
|
<Button
|
||||||
|
:as="RouterLink"
|
||||||
|
:class="{ '!bg-accent !text-primary-foreground': currentRoute.path === route.url, 'w-full': isVertical }"
|
||||||
|
:variant="isVertical ? 'outline' : 'ghost'"
|
||||||
|
@click="handleNavDrawer"
|
||||||
|
:to="route.url"
|
||||||
|
:title="route.title"
|
||||||
|
>
|
||||||
|
<component :is="route.icon" />
|
||||||
|
<p class="text-sm">{{ route.title }}</p>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
53
src/components/NavSheet.vue
Normal file
53
src/components/NavSheet.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetFooter,
|
||||||
|
} from '@/components/ui/sheet'
|
||||||
|
|
||||||
|
import Nav from '@/components/Nav.vue';
|
||||||
|
import TitleName from '@/components/TitleName.vue';
|
||||||
|
import SocialLinks from "@/components/SocialLinks.vue"
|
||||||
|
|
||||||
|
import { Menu, X } from '@lucide/vue';
|
||||||
|
|
||||||
|
const open = ref<boolean>(false)
|
||||||
|
|
||||||
|
const closeSheet = () => open.value = false
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Sheet v-bind="{open}">
|
||||||
|
<SheetTrigger as-child>
|
||||||
|
<Button variant="ghost" @click="open = true" title="Navigation Menu">
|
||||||
|
<Menu />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent
|
||||||
|
side="left"
|
||||||
|
class="[&>button]:hidden"
|
||||||
|
:onOpenAutoFocus="(e: Event) => e.preventDefault()"
|
||||||
|
:onCloseAutoFocus="(e: Event) => e.preventDefault()"
|
||||||
|
>
|
||||||
|
<SheetHeader class="border-b-1 py-3">
|
||||||
|
<SheetTitle class="flex justify-between">
|
||||||
|
<TitleName />
|
||||||
|
<Button variant="ghost" @click="closeSheet" title="Close Navigation Menu">
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</SheetTitle>
|
||||||
|
</SheetHeader>
|
||||||
|
<div class="px-4">
|
||||||
|
<Nav aria-label="Mobile" orientation="vertical" @route:clicked="closeSheet" />
|
||||||
|
</div>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
58
src/components/SocialLinks.vue
Normal file
58
src/components/SocialLinks.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { shallowRef } from 'vue';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
import Github from "@/components/icons/Github.vue";
|
||||||
|
import LinkedIn from "@/components/icons/LinkedIn.vue";
|
||||||
|
import ForgeJo from "@/components/icons/ForgeJo.vue";
|
||||||
|
|
||||||
|
import { Mail } from "@lucide/vue"
|
||||||
|
|
||||||
|
const emit = defineEmits<{"route:clicked": []}>()
|
||||||
|
|
||||||
|
const buttons = shallowRef( [
|
||||||
|
{
|
||||||
|
title: "Github",
|
||||||
|
icon: Github,
|
||||||
|
url: "https://github.com/McPeakDev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Forgejo",
|
||||||
|
icon: ForgeJo,
|
||||||
|
url: "https://git.mcpeakdev.com/McPeakML"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "LinkedIn",
|
||||||
|
icon: LinkedIn,
|
||||||
|
url: "https://linkedin.com/in/matthewmcpeak"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Email",
|
||||||
|
icon: Mail,
|
||||||
|
url: "mailto:m.mcpeak98@icloud.com"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleNavDrawer = () => {
|
||||||
|
emit('route:clicked')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex gap-3 justify-center flex-row w-full">
|
||||||
|
<Button
|
||||||
|
as="a"
|
||||||
|
v-for="button in buttons"
|
||||||
|
:key="button.title"
|
||||||
|
variant="outline"
|
||||||
|
:title="button.title"
|
||||||
|
:href="button.url"
|
||||||
|
@click="handleNavDrawer"
|
||||||
|
>
|
||||||
|
<component :is="button.icon" />
|
||||||
|
<p class="text-sm">{{ button.title }}</p>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
31
src/components/ThemeToggleBtn.vue
Normal file
31
src/components/ThemeToggleBtn.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, type Component } from "vue"
|
||||||
|
import { useColorMode } from "@vueuse/core"
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
import { Sun, Moon } from '@lucide/vue'
|
||||||
|
|
||||||
|
import { Theme } from "@/types/theme"
|
||||||
|
|
||||||
|
|
||||||
|
const mode = useColorMode({
|
||||||
|
disableTransition: false
|
||||||
|
})
|
||||||
|
const theme = computed<Theme>(() => getSystemTheme() === "light" ? Theme.Light : Theme.Dark)
|
||||||
|
const themeIcon = computed<Component>(() => theme.value === Theme.Light ? Sun : Moon)
|
||||||
|
const themeBtnText = computed<string>(() => theme.value === Theme.Light ? "Light" : "Dark")
|
||||||
|
|
||||||
|
const getSystemTheme = () => (mode.store.value === 'auto' ? mode.system.value : mode.store.value)
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
mode.value = theme.value === Theme.Light ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Button class="cursor-pointer" variant="ghost" @click="toggleTheme" :title="`Toggle ${ themeBtnText } Theme`">
|
||||||
|
<component :is="themeIcon"/>
|
||||||
|
<p class="hidden md:inline">{{ themeBtnText }}</p>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
3
src/components/TitleName.vue
Normal file
3
src/components/TitleName.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p class="font-bold text-2xl my-1 antialiased">Matthew McPeak</p>
|
||||||
|
</template>
|
||||||
3
src/components/icons/ForgeJo.vue
Normal file
3
src/components/icons/ForgeJo.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Forgejo</title><path d="M16.7773 0c1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.1175a7.0759 7.0759 0 0 1 4.148-1.4205l.1176-.001 1.3385.0002c.4973-.8827 1.4434-1.4788 2.5288-1.4788 1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.319c.8827.4973 1.4788 1.4434 1.4788 2.5287 0 1.602-1.2986 2.9005-2.9005 2.9005-1.6018 0-2.9004-1.2986-2.9004-2.9005 0-1.0853.596-2.0314 1.4788-2.5287l-.0002-9.9831c0-3.887 3.1195-7.0453 6.9915-7.108l.1176-.001h1.3385C14.7458.5962 15.692 0 16.7773 0ZM7.2227 19.9052c-.6596 0-1.1943.5347-1.1943 1.1943s.5347 1.1943 1.1943 1.1943 1.1944-.5347 1.1944-1.1943-.5348-1.1943-1.1944-1.1943Zm9.5546-10.4644c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Zm0-7.7346c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Z"/></svg>
|
||||||
|
</template>
|
||||||
3
src/components/icons/Github.vue
Normal file
3
src/components/icons/Github.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||||
|
</template>
|
||||||
3
src/components/icons/LinkedIn.vue
Normal file
3
src/components/icons/LinkedIn.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<svg role="img" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><title>LinkedIn</title><path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/></svg>
|
||||||
|
</template>
|
||||||
26
src/components/ui/badge/Badge.vue
Normal file
26
src/components/ui/badge/Badge.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import type { BadgeVariants } from "."
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { badgeVariants } from "."
|
||||||
|
|
||||||
|
const props = defineProps<PrimitiveProps & {
|
||||||
|
variant?: BadgeVariants["variant"]
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="badge"
|
||||||
|
:class="cn(badgeVariants({ variant }), props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
26
src/components/ui/badge/index.ts
Normal file
26
src/components/ui/badge/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export { default as Badge } from "./Badge.vue"
|
||||||
|
|
||||||
|
export const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||||
31
src/components/ui/button/Button.vue
Normal file
31
src/components/ui/button/Button.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PrimitiveProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import type { ButtonVariants } from "."
|
||||||
|
import { Primitive } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { buttonVariants } from "."
|
||||||
|
|
||||||
|
interface Props extends PrimitiveProps {
|
||||||
|
variant?: ButtonVariants["variant"]
|
||||||
|
size?: ButtonVariants["size"]
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
as: "button",
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive
|
||||||
|
data-slot="button"
|
||||||
|
:data-variant="variant"
|
||||||
|
:data-size="size"
|
||||||
|
:as="as"
|
||||||
|
:as-child="asChild"
|
||||||
|
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
40
src/components/ui/button/index.ts
Normal file
40
src/components/ui/button/index.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { VariantProps } from "class-variance-authority"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export { default as Button } from "./Button.vue"
|
||||||
|
|
||||||
|
export const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
"xs": "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
"icon": "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
||||||
22
src/components/ui/card/Card.vue
Normal file
22
src/components/ui/card/Card.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
src/components/ui/card/CardAction.vue
Normal file
17
src/components/ui/card/CardAction.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
src/components/ui/card/CardContent.vue
Normal file
17
src/components/ui/card/CardContent.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
:class="cn('px-6', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
src/components/ui/card/CardDescription.vue
Normal file
17
src/components/ui/card/CardDescription.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p
|
||||||
|
data-slot="card-description"
|
||||||
|
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
17
src/components/ui/card/CardFooter.vue
Normal file
17
src/components/ui/card/CardFooter.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
src/components/ui/card/CardHeader.vue
Normal file
17
src/components/ui/card/CardHeader.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
src/components/ui/card/CardTitle.vue
Normal file
17
src/components/ui/card/CardTitle.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h3
|
||||||
|
data-slot="card-title"
|
||||||
|
:class="cn('leading-none font-semibold', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</h3>
|
||||||
|
</template>
|
||||||
7
src/components/ui/card/index.ts
Normal file
7
src/components/ui/card/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export { default as Card } from "./Card.vue"
|
||||||
|
export { default as CardAction } from "./CardAction.vue"
|
||||||
|
export { default as CardContent } from "./CardContent.vue"
|
||||||
|
export { default as CardDescription } from "./CardDescription.vue"
|
||||||
|
export { default as CardFooter } from "./CardFooter.vue"
|
||||||
|
export { default as CardHeader } from "./CardHeader.vue"
|
||||||
|
export { default as CardTitle } from "./CardTitle.vue"
|
||||||
35
src/components/ui/navigation-menu/NavigationMenu.vue
Normal file
35
src/components/ui/navigation-menu/NavigationMenu.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuRootEmits, NavigationMenuRootProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
NavigationMenuRoot,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import NavigationMenuViewport from "./NavigationMenuViewport.vue"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<NavigationMenuRootProps & {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
viewport?: boolean
|
||||||
|
}>(), {
|
||||||
|
viewport: true,
|
||||||
|
})
|
||||||
|
const emits = defineEmits<NavigationMenuRootEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "viewport")
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NavigationMenuRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="navigation-menu"
|
||||||
|
:data-viewport="viewport"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('group/navigation-menu relative flex max-w-max flex-1 items-center justify-center', props.class)"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
<NavigationMenuViewport v-if="viewport" />
|
||||||
|
</NavigationMenuRoot>
|
||||||
|
</template>
|
||||||
31
src/components/ui/navigation-menu/NavigationMenuContent.vue
Normal file
31
src/components/ui/navigation-menu/NavigationMenuContent.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuContentEmits, NavigationMenuContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
NavigationMenuContent,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<NavigationMenuContentProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
const emits = defineEmits<NavigationMenuContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NavigationMenuContent
|
||||||
|
data-slot="navigation-menu-content"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(
|
||||||
|
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
|
||||||
|
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</NavigationMenuContent>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuIndicatorProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { NavigationMenuIndicator, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<NavigationMenuIndicatorProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NavigationMenuIndicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden', props.class)"
|
||||||
|
>
|
||||||
|
<div class="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||||
|
</NavigationMenuIndicator>
|
||||||
|
</template>
|
||||||
21
src/components/ui/navigation-menu/NavigationMenuItem.vue
Normal file
21
src/components/ui/navigation-menu/NavigationMenuItem.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuItemProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { NavigationMenuItem } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<NavigationMenuItemProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NavigationMenuItem
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('relative', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</NavigationMenuItem>
|
||||||
|
</template>
|
||||||
26
src/components/ui/navigation-menu/NavigationMenuLink.vue
Normal file
26
src/components/ui/navigation-menu/NavigationMenuLink.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuLinkEmits, NavigationMenuLinkProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
NavigationMenuLink,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<NavigationMenuLinkProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
const emits = defineEmits<NavigationMenuLinkEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NavigationMenuLink
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(`data-active:focus:bg-accent data-active:hover:bg-accent data-active:bg-accent/50 data-active:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4`, props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</template>
|
||||||
28
src/components/ui/navigation-menu/NavigationMenuList.vue
Normal file
28
src/components/ui/navigation-menu/NavigationMenuList.vue
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuListProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { NavigationMenuList, useForwardProps } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<NavigationMenuListProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NavigationMenuList
|
||||||
|
data-slot="navigation-menu-list"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'group flex flex-1 list-none items-center justify-center gap-1',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</NavigationMenuList>
|
||||||
|
</template>
|
||||||
32
src/components/ui/navigation-menu/NavigationMenuTrigger.vue
Normal file
32
src/components/ui/navigation-menu/NavigationMenuTrigger.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuTriggerProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { ChevronDown } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
useForwardProps,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { navigationMenuTriggerStyle } from "."
|
||||||
|
|
||||||
|
const props = defineProps<NavigationMenuTriggerProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NavigationMenuTrigger
|
||||||
|
data-slot="navigation-menu-trigger"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(navigationMenuTriggerStyle(), 'group', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<ChevronDown
|
||||||
|
class="relative top-px ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
</template>
|
||||||
31
src/components/ui/navigation-menu/NavigationMenuViewport.vue
Normal file
31
src/components/ui/navigation-menu/NavigationMenuViewport.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuViewportProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
NavigationMenuViewport,
|
||||||
|
useForwardProps,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<NavigationMenuViewportProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="absolute top-full left-0 isolate z-50 flex justify-center">
|
||||||
|
<NavigationMenuViewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-(--reka-navigation-menu-viewport-height) w-full overflow-hidden rounded-md border shadow md:w-(--reka-navigation-menu-viewport-width) left-(--reka-navigation-menu-viewport-left)',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
14
src/components/ui/navigation-menu/index.ts
Normal file
14
src/components/ui/navigation-menu/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
|
export { default as NavigationMenu } from "./NavigationMenu.vue"
|
||||||
|
export { default as NavigationMenuContent } from "./NavigationMenuContent.vue"
|
||||||
|
export { default as NavigationMenuIndicator } from "./NavigationMenuIndicator.vue"
|
||||||
|
export { default as NavigationMenuItem } from "./NavigationMenuItem.vue"
|
||||||
|
export { default as NavigationMenuLink } from "./NavigationMenuLink.vue"
|
||||||
|
export { default as NavigationMenuList } from "./NavigationMenuList.vue"
|
||||||
|
export { default as NavigationMenuTrigger } from "./NavigationMenuTrigger.vue"
|
||||||
|
export { default as NavigationMenuViewport } from "./NavigationMenuViewport.vue"
|
||||||
|
|
||||||
|
export const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-3 focus-visible:outline-1",
|
||||||
|
)
|
||||||
19
src/components/ui/sheet/Sheet.vue
Normal file
19
src/components/ui/sheet/Sheet.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
|
||||||
|
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogRootProps>()
|
||||||
|
const emits = defineEmits<DialogRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogRoot
|
||||||
|
v-slot="slotProps"
|
||||||
|
data-slot="sheet"
|
||||||
|
v-bind="forwarded"
|
||||||
|
>
|
||||||
|
<slot v-bind="slotProps" />
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
15
src/components/ui/sheet/SheetClose.vue
Normal file
15
src/components/ui/sheet/SheetClose.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogCloseProps } from "reka-ui"
|
||||||
|
import { DialogClose } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogCloseProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogClose
|
||||||
|
data-slot="sheet-close"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogClose>
|
||||||
|
</template>
|
||||||
62
src/components/ui/sheet/SheetContent.vue
Normal file
62
src/components/ui/sheet/SheetContent.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { X } from "@lucide/vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import {
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import SheetOverlay from "./SheetOverlay.vue"
|
||||||
|
|
||||||
|
interface SheetContentProps extends DialogContentProps {
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
side?: "top" | "right" | "bottom" | "left"
|
||||||
|
}
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<SheetContentProps>(), {
|
||||||
|
side: "right",
|
||||||
|
})
|
||||||
|
const emits = defineEmits<DialogContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class", "side")
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<DialogContent
|
||||||
|
data-slot="sheet-content"
|
||||||
|
:class="cn(
|
||||||
|
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||||
|
side === 'right'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||||
|
side === 'left'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
|
||||||
|
side === 'top'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
|
||||||
|
side === 'bottom'
|
||||||
|
&& 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
|
||||||
|
props.class)"
|
||||||
|
v-bind="{ ...$attrs, ...forwarded }"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<DialogClose
|
||||||
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<X class="size-4" />
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</template>
|
||||||
21
src/components/ui/sheet/SheetDescription.vue
Normal file
21
src/components/ui/sheet/SheetDescription.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogDescriptionProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogDescription } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogDescription
|
||||||
|
data-slot="sheet-description"
|
||||||
|
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogDescription>
|
||||||
|
</template>
|
||||||
16
src/components/ui/sheet/SheetFooter.vue
Normal file
16
src/components/ui/sheet/SheetFooter.vue
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
:class="cn('mt-auto flex flex-col gap-2 p-4', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
15
src/components/ui/sheet/SheetHeader.vue
Normal file
15
src/components/ui/sheet/SheetHeader.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
:class="cn('flex flex-col gap-1.5 p-4', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
21
src/components/ui/sheet/SheetOverlay.vue
Normal file
21
src/components/ui/sheet/SheetOverlay.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogOverlayProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogOverlay } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogOverlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogOverlay>
|
||||||
|
</template>
|
||||||
21
src/components/ui/sheet/SheetTitle.vue
Normal file
21
src/components/ui/sheet/SheetTitle.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTitleProps } from "reka-ui"
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { DialogTitle } from "reka-ui"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTitle
|
||||||
|
data-slot="sheet-title"
|
||||||
|
:class="cn('text-foreground font-semibold', props.class)"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogTitle>
|
||||||
|
</template>
|
||||||
15
src/components/ui/sheet/SheetTrigger.vue
Normal file
15
src/components/ui/sheet/SheetTrigger.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DialogTriggerProps } from "reka-ui"
|
||||||
|
import { DialogTrigger } from "reka-ui"
|
||||||
|
|
||||||
|
const props = defineProps<DialogTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DialogTrigger
|
||||||
|
data-slot="sheet-trigger"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DialogTrigger>
|
||||||
|
</template>
|
||||||
8
src/components/ui/sheet/index.ts
Normal file
8
src/components/ui/sheet/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export { default as Sheet } from "./Sheet.vue"
|
||||||
|
export { default as SheetClose } from "./SheetClose.vue"
|
||||||
|
export { default as SheetContent } from "./SheetContent.vue"
|
||||||
|
export { default as SheetDescription } from "./SheetDescription.vue"
|
||||||
|
export { default as SheetFooter } from "./SheetFooter.vue"
|
||||||
|
export { default as SheetHeader } from "./SheetHeader.vue"
|
||||||
|
export { default as SheetTitle } from "./SheetTitle.vue"
|
||||||
|
export { default as SheetTrigger } from "./SheetTrigger.vue"
|
||||||
7
src/lib/utils.ts
Normal file
7
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { ClassValue } from "clsx"
|
||||||
|
import { clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
11
src/main.ts
Normal file
11
src/main.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from '@/App.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
import "@/assets/index.css"
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
21
src/router/index.ts
Normal file
21
src/router/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path:"/",
|
||||||
|
component: () => import("@/views/IndexView.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:"/skills",
|
||||||
|
component: () => import("@/views/SkillsView.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path:"/work",
|
||||||
|
component: () => import("@/views/WorkView.vue")
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
5
src/types/routes.ts
Normal file
5
src/types/routes.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export enum Routes {
|
||||||
|
Home = "/",
|
||||||
|
Skills = "/skills",
|
||||||
|
Work = "/work",
|
||||||
|
}
|
||||||
4
src/types/theme.ts
Normal file
4
src/types/theme.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum Theme {
|
||||||
|
Light,
|
||||||
|
Dark
|
||||||
|
}
|
||||||
11
src/views/IndexView.vue
Normal file
11
src/views/IndexView.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 w-full mb-4">
|
||||||
|
<h1 class="text-4xl md:text-5xl italic font-bold ">Hey, my name is Matthew McPeak!</h1>
|
||||||
|
<h2 class="text-xl italic text-muted-foreground">I write code that holds up: reliable, maintainable, and built to last under real constraints.</h2>
|
||||||
|
<p>Based in Kingsport, TN. I'm deliberate by nature. I'd rather spend the time to get something right than ship something fast that someone else has to fix later. Six years building SaaS for county clerks across Tennessee will do that: a small user base with a wide blast radius, traffic that spikes hard around filing deadlines, and real consequences for cutting corners. The work I'm proudest of started that way. I hand-built a prototype workflow engine to solve a problem I kept hitting, it worked well enough that our VP of Software rebuilt it for production, and it now backs most of the workflows in the product.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row mt-4 gap-10 w-full justify-evenly text-center" >
|
||||||
|
<h2 class="text-xl italic text-muted-foreground"><span class="font-bold text-primary-foreground bg-primary rounded-lg p-2">6+</span> Years of Experience</h2>
|
||||||
|
<h2 class="text-xl italic text-muted-foreground"><span class="font-bold text-primary-foreground bg-primary rounded-lg p-2">10+</span> Workflows on an engine I designed</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
68
src/views/SkillsView.vue
Normal file
68
src/views/SkillsView.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge
|
||||||
|
} from '@/components/ui/badge'
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 w-full mb-4">
|
||||||
|
<h1 class="text-5xl italic font-bold ">Skills</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row gap-2 w-full">
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardHeader class="pb-2 border-b-2">
|
||||||
|
<CardTitle>Front-End</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-row gap-2 w-full">
|
||||||
|
<Badge>TypeScript</Badge>
|
||||||
|
<Badge>Vue 3</Badge>
|
||||||
|
<Badge>Nuxt 3</Badge>
|
||||||
|
<Badge>shadcn</Badge>
|
||||||
|
<Badge>Tailwind</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardHeader class="pb-2 border-b-2">
|
||||||
|
<CardTitle>Back-End</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-row gap-2 w-full">
|
||||||
|
<Badge>Go</Badge>
|
||||||
|
<Badge>MySQL</Badge>
|
||||||
|
<Badge>REST APIs</Badge>
|
||||||
|
<Badge>C#</Badge>
|
||||||
|
<Badge>ASP.NET Web API</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row gap-2 w-full">
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardHeader class="pb-2 border-b-2">
|
||||||
|
<CardTitle>Practices</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-row gap-2 w-full">
|
||||||
|
<Badge>WCAG</Badge>
|
||||||
|
<Badge>Component architecture</Badge>
|
||||||
|
<Badge>Agentic workflows</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardHeader class="pb-2 border-b-2">
|
||||||
|
<CardTitle>Tools</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-row gap-2 w-full">
|
||||||
|
<Badge>Git</Badge>
|
||||||
|
<Badge>Vite</Badge>
|
||||||
|
<Badge>Docker</Badge>
|
||||||
|
<Badge>Claude Code</Badge>
|
||||||
|
<Badge>Cursor</Badge>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
56
src/views/WorkView.vue
Normal file
56
src/views/WorkView.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge
|
||||||
|
} from '@/components/ui/badge'
|
||||||
|
import { Minus } from '@lucide/vue';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 w-full mb-4">
|
||||||
|
<h1 class="text-5xl italic font-bold ">Work History</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row gap-2 w-full">
|
||||||
|
<Card class="w-full">
|
||||||
|
<CardHeader class="pb-2 border-b-2">
|
||||||
|
<CardTitle>Software Developer - i3 Verticals</CardTitle>
|
||||||
|
<CardDescription class="italic">Jun 2020 - Present</CardDescription>
|
||||||
|
<CardDescription class="italic">Government SaaS for county clerks across Tennessee. Vue 3 and TypeScript front ends against REST microservices.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="flex flex-col gap-2 w-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Badge class="text-md">Workflow Engine</Badge>
|
||||||
|
<div class="flex flex-row gap-2 pl-4">
|
||||||
|
<span>
|
||||||
|
<Minus :size="16" class="basis-1/12 pt-1" />
|
||||||
|
</span>
|
||||||
|
<p class="italic text-sm">Designed it and hand-built the prototype that proved the approach. Our VP of Software rebuilt it for production in Go, and it now backs 10+ distinct workflows across the product, areas that share nothing except being workflows. I ran one of the migrations onto it solo, over several weeks, in a live system. I'm still the person called in when it misbehaves.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Badge class="text-md">Self-Service Kiosks</Badge>
|
||||||
|
<div class="flex flex-row gap-2 pl-4">
|
||||||
|
<span>
|
||||||
|
<Minus :size="16" class="basis-1/12 pt-1" />
|
||||||
|
</span>
|
||||||
|
<p class="italic text-sm">Improved the UI and the device communications layer on kiosks built to handle new vehicle plate requirements. Talking to physical hardware from a web stack brings a different set of failure modes than a browser: timeouts, partial reads, and no user standing there to retry.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Badge class="text-md">Reporting</Badge>
|
||||||
|
<div class="flex flex-row gap-2 pl-4">
|
||||||
|
<span>
|
||||||
|
<Minus :size="16" class="basis-1/12 pt-1" />
|
||||||
|
</span>
|
||||||
|
<p class="italic text-sm">Added PDF, CSV, Excel and HTML output formats to the reporting engine, then wrote a Claude Code skill so any of our 4 to 8 developers can generate new reports without me in the loop.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
18
tsconfig.app.json
Normal file
18
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Extra safety for array and object lookups, but may have false positives.
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
|
||||||
|
// Path mapping for cleaner imports.
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
tsconfig.node.json
Normal file
28
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node24/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"playwright.config.*",
|
||||||
|
"eslint.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||||
|
// Bundler mode provides a smoother developer experience.
|
||||||
|
"module": "preserve",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
|
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||||
|
"types": ["node"],
|
||||||
|
|
||||||
|
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||||
|
// Specified here to keep it out of the root directory.
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
vite.config.ts
Normal file
18
vite.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
tailwindcss()
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue