Files
GitRight/index.html

564 lines
31 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>GitRight Mobile UI</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<style>
[x-cloak] { display: none !important; }
.custom-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
.truncate-2-lines { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.select-none { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
</style>
</head>
<body class="bg-white antialiased text-slate-900 overflow-hidden select-none">
<div x-data="{
leftSidebar: false,
rightSidebar: false,
accountModal: false,
commitModal: false,
githubPat: localStorage.getItem('github_pat') || '',
repos: JSON.parse(localStorage.getItem('github_repos') || '[]'),
currentRepo: null,
fileTree: [],
currentPath: '',
isLoading: false,
isPushing: false,
contextMenu: { show: false, x: 0, y: 0, target: null, type: 'deadspace' },
pressTimer: null,
clipboard: { item: null, action: null },
pendingChanges: [],
commitMessage: '',
init() {
this.$watch('currentPath', () => this.refreshIcons());
this.$watch('fileTree', () => this.refreshIcons());
this.$watch('contextMenu.show', (val) => val && this.refreshIcons());
this.$watch('commitModal', (val) => val && this.refreshIcons());
this.$watch('clipboard', () => this.refreshIcons());
},
refreshIcons() {
this.$nextTick(() => lucide.createIcons());
},
async apiRequest(endpoint, method = 'GET', body = null) {
const [fullRepo] = this.currentRepo.split('@');
const url = `https://api.github.com/repos/${fullRepo}${endpoint}`;
const headers = {
'Accept': 'application/vnd.github.v3+json',
'Authorization': `token ${this.githubPat}`,
'Content-Type': 'application/json'
};
const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : null });
if (!response.ok) {
const err = await response.json();
throw new Error(err.message || 'API Error');
}
return response.json();
},
async selectRepo(repoStr) {
this.currentRepo = repoStr;
this.currentPath = '';
this.isLoading = true;
this.rightSidebar = false;
this.pendingChanges = [];
const [fullRepo, branchPart] = repoStr.split('@');
const branch = branchPart || 'main';
try {
const data = await this.apiRequest(`/git/trees/${branch}?recursive=1`);
this.fileTree = data.tree || [];
} catch (e) {
alert('Error fetching repository, Meowster: ' + e.message);
this.currentRepo = null;
} finally {
this.isLoading = false;
this.refreshIcons();
}
},
get currentItems() {
if (!this.fileTree.length) return [];
return this.fileTree.filter(item => {
const parts = item.path.split('/');
if (this.currentPath === '') return parts.length === 1;
return parts.slice(0, -1).join('/') === this.currentPath;
}).sort((a, b) => (b.type === 'tree') - (a.type === 'tree') || a.path.localeCompare(b.path));
},
get breadcrumbs() {
return this.currentPath ? this.currentPath.split('/') : [];
},
get repoDisplayName() {
return this.currentRepo ? this.currentRepo.split('@')[0].split('/').pop() : '';
},
navigateTo(path) {
const item = this.fileTree.find(i => i.path === path);
if (item && item.type === 'tree') this.currentPath = path;
},
goUp() {
if (!this.currentPath) return;
const parts = this.currentPath.split('/');
parts.pop();
this.currentPath = parts.join('/');
},
handlePressStart(e, item, type) {
this.pressTimer = setTimeout(() => {
if ('vibrate' in navigator) navigator.vibrate(40);
this.showContextMenu(e, item, type);
}, 400);
},
handlePressEnd() {
clearTimeout(this.pressTimer);
},
showContextMenu(e, item, type) {
const touch = e.touches ? e.touches[0] : e;
this.contextMenu = {
show: true,
x: Math.min(touch.clientX, window.innerWidth - 160),
y: Math.min(touch.clientY, window.innerHeight - 200),
target: item,
type: type
};
},
renameItem() {
const oldPath = this.contextMenu.target.path;
const oldName = oldPath.split('/').pop();
const newName = prompt('Rename to:', oldName);
if (newName && newName !== oldName) {
const newPath = oldPath.substring(0, oldPath.lastIndexOf(oldName)) + newName;
const itemsToMove = this.fileTree.filter(i => i.path === oldPath || i.path.startsWith(oldPath + '/'));
this.fileTree = this.fileTree.map(item => {
const match = itemsToMove.find(m => m.path === item.path);
if (match) {
const updatedPath = item.path.replace(oldPath, newPath);
this.trackChange('rename', updatedPath, item.path, item.sha, item.type);
return { ...item, path: updatedPath };
}
return item;
});
}
this.contextMenu.show = false;
},
deleteItem() {
if (confirm('Are you sure, Meowster?')) {
const target = this.contextMenu.target;
const itemsToDelete = this.fileTree.filter(i => i.path === target.path || i.path.startsWith(target.path + '/'));
itemsToDelete.forEach(item => {
this.trackChange('delete', item.path, null, item.sha, item.type);
});
this.fileTree = this.fileTree.filter(item =>
!itemsToDelete.some(d => d.path === item.path)
);
}
this.contextMenu.show = false;
},
copyItem(isCut = false) {
this.clipboard = {
item: JSON.parse(JSON.stringify(this.contextMenu.target)),
action: isCut ? 'cut' : 'copy'
};
this.contextMenu.show = false;
},
pasteItem() {
if (!this.clipboard.item) return;
const oldPrefix = this.clipboard.item.path;
const name = oldPrefix.split('/').pop();
const newPrefix = this.currentPath ? `${this.currentPath}/${name}` : name;
if (this.fileTree.some(i => i.path === newPrefix)) return alert('Collision detected, Meowster.');
if (this.clipboard.action === 'copy' && newPrefix.startsWith(oldPrefix + '/')) return alert('Cannot copy a folder into itself, Meowster.');
const itemsToProcess = this.fileTree.filter(i => i.path === oldPrefix || i.path.startsWith(oldPrefix + '/'));
if (this.clipboard.action === 'cut') {
this.fileTree = this.fileTree.map(item => {
const match = itemsToProcess.find(p => p.path === item.path);
if (match) {
const itemNewPath = item.path.replace(oldPrefix, newPrefix);
this.trackChange('move', itemNewPath, item.path, item.sha, item.type);
return { ...item, path: itemNewPath };
}
return item;
});
this.clipboard = { item: null, action: null };
} else {
const newItems = itemsToProcess.map(item => {
const itemNewPath = item.path.replace(oldPrefix, newPrefix);
this.trackChange('add', itemNewPath, null, item.sha, item.type);
return { ...item, path: itemNewPath, sha: item.sha };
});
this.fileTree.push(...newItems);
}
this.contextMenu.show = false;
},
createFolder() {
const name = prompt('Folder name:', 'new-folder');
if (name) {
const newPath = this.currentPath ? `${this.currentPath}/${name}` : name;
const keepPath = `${newPath}/.gitkeep`;
this.fileTree.push({ path: newPath, type: 'tree', sha: null });
this.trackChange('add', keepPath, null, null, 'blob');
}
this.contextMenu.show = false;
},
trackChange(type, path, oldPath = null, sha = null, itemType = 'blob') {
this.pendingChanges.push({ type, path, oldPath, sha, itemType, id: Date.now() });
},
async pushChanges() {
if (!this.commitMessage) return alert('Enter a message, Meowster.');
this.isPushing = true;
try {
const branch = this.currentRepo.split('@')[1] || 'main';
const refData = await this.apiRequest(`/git/refs/heads/${branch}`);
const lastCommitSha = refData.object.sha;
const lastCommitData = await this.apiRequest(`/git/commits/${lastCommitSha}`);
const baseTreeSha = lastCommitData.tree.sha;
const treePayload = [];
this.pendingChanges.forEach(change => {
const mode = change.itemType === 'tree' ? '040000' : '100644';
if (change.type === 'delete') {
treePayload.push({ path: change.path, mode, type: change.itemType, sha: null });
} else if (change.type === 'rename' || change.type === 'move') {
treePayload.push({ path: change.oldPath, mode, type: change.itemType, sha: null });
treePayload.push({ path: change.path, mode, type: change.itemType, sha: change.sha });
} else if (change.type === 'add') {
if (change.sha) {
treePayload.push({ path: change.path, mode, type: change.itemType, sha: change.sha });
} else {
treePayload.push({ path: change.path, mode, type: change.itemType, content: '\n' });
}
}
});
const newTree = await this.apiRequest('/git/trees', 'POST', {
base_tree: baseTreeSha,
tree: treePayload
});
const newCommit = await this.apiRequest('/git/commits', 'POST', {
message: this.commitMessage,
tree: newTree.sha,
parents: [lastCommitSha]
});
await this.apiRequest(`/git/refs/heads/${branch}`, 'PATCH', {
sha: newCommit.sha,
force: false
});
alert('Successfully pushed to GitHub, Meowster!');
this.pendingChanges = [];
this.commitMessage = '';
this.commitModal = false;
await this.selectRepo(this.currentRepo);
} catch (e) {
alert('Push failed: ' + e.message);
} finally {
this.isPushing = false;
}
},
addRepo() {
const repo = prompt('Enter repository (owner/repo@branch):', 'multipleof4/GitRight@master');
if (repo && repo.includes('/')) {
this.repos.push(repo);
this.saveRepos();
}
},
removeRepo(index) {
this.repos.splice(index, 1);
this.saveRepos();
},
saveRepos() {
localStorage.setItem('github_repos', JSON.stringify(this.repos));
}
}" class="h-screen flex flex-col overflow-hidden">
<!-- Header -->
<header class="flex items-center justify-between px-4 py-2 border-b border-gray-100 bg-white z-50 shrink-0">
<button @click="leftSidebar = !leftSidebar" class="p-1.5 hover:bg-gray-50 rounded-md border border-gray-200 text-slate-600">
<i data-lucide="panel-left" class="w-4 h-4"></i>
</button>
<button
@click="pendingChanges.length ? commitModal = true : null"
:class="pendingChanges.length ? 'bg-orange-500 shadow-lg shadow-orange-200' : 'bg-slate-100'"
class="p-1.5 rounded-full transition-all duration-300"
>
<i :data-lucide="pendingChanges.length ? 'git-commit' : 'sun'"
:class="pendingChanges.length ? 'text-white' : 'text-slate-600'"
class="w-4 h-4"></i>
</button>
<button @click="rightSidebar = !rightSidebar" class="p-1.5 hover:bg-gray-50 rounded-md border border-gray-200 text-slate-600">
<i data-lucide="panel-right" class="w-4 h-4"></i>
</button>
</header>
<!-- Main Content Area -->
<main
class="flex-1 flex flex-col min-h-0 bg-slate-50/50 relative"
@touchstart="handlePressStart($event, null, 'deadspace')"
@touchend="handlePressEnd()"
@mousedown="handlePressStart($event, null, 'deadspace')"
@mouseup="handlePressEnd()"
>
<template x-if="currentRepo">
<div class="flex items-center px-4 py-2 bg-white border-b border-gray-100 space-x-2 shrink-0 z-10">
<button @click="goUp()" :disabled="!currentPath" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">
<i data-lucide="arrow-up" class="w-4 h-4 text-slate-600"></i>
</button>
<div class="flex-1 flex items-center bg-slate-50 border border-slate-200 rounded px-2 py-1 overflow-x-auto custom-scrollbar">
<button @click="currentPath = ''" class="text-xs font-bold text-blue-600 hover:underline whitespace-nowrap" x-text="repoDisplayName"></button>
<template x-for="(part, index) in breadcrumbs" :key="index">
<div class="flex items-center">
<i data-lucide="chevron-right" class="w-3 h-3 mx-1 text-slate-400"></i>
<button @click="navigateTo(breadcrumbs.slice(0, index + 1).join('/'))" class="text-xs text-blue-600 hover:underline whitespace-nowrap" x-text="part"></button>
</div>
</template>
</div>
</div>
</template>
<div class="flex-1 overflow-y-auto p-4 custom-scrollbar">
<template x-if="!currentRepo">
<div class="h-full flex flex-col items-center justify-center text-center space-y-4">
<div class="bg-white p-6 rounded-3xl shadow-sm border border-slate-100">
<i data-lucide="folder-open" class="w-12 h-12 text-slate-200 mx-auto mb-4"></i>
<h1 class="text-lg font-medium text-slate-400">No Repository Selected</h1>
<p class="text-xs text-slate-300">Select a repo from the right sidebar, Meowster.</p>
</div>
</div>
</template>
<template x-if="currentRepo && isLoading">
<div class="h-full flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
</template>
<template x-if="currentRepo && !isLoading">
<div class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
<template x-for="item in currentItems" :key="item.path">
<button
@click="navigateTo(item.path)"
@touchstart.stop="handlePressStart($event, item, 'item')"
@touchend.stop="handlePressEnd()"
@mousedown.stop="handlePressStart($event, item, 'item')"
@mouseup.stop="handlePressEnd()"
:class="{
'opacity-40 grayscale pointer-events-none': clipboard.item && clipboard.action === 'cut' && (item.path === clipboard.item.path || item.path.startsWith(clipboard.item.path + '/')),
'flex flex-col items-center p-2 rounded-xl hover:bg-white hover:shadow-sm border border-transparent hover:border-slate-200 transition-all group': true
}"
>
<div class="mb-2">
<template x-if="item.type === 'tree'">
<i data-lucide="folder" class="w-10 h-10 text-blue-400 fill-blue-50"></i>
</template>
<template x-if="item.type === 'blob'">
<i data-lucide="file-text" class="w-10 h-10 text-slate-400"></i>
</template>
</div>
<span class="text-[10px] font-medium text-slate-600 text-center break-all truncate-2-lines leading-tight" x-text="item.path.split('/').pop()"></span>
</button>
</template>
</div>
</template>
</div>
</main>
<!-- Context Menu -->
<div x-show="contextMenu.show" x-cloak class="fixed inset-0 z-[100]">
<div @click="contextMenu.show = false" class="absolute inset-0"></div>
<div class="absolute bg-white rounded-xl shadow-2xl border border-slate-200 py-1 w-40 overflow-hidden" :style="`top: ${contextMenu.y}px; left: ${contextMenu.x}px;`">
<template x-if="contextMenu.type === 'item'">
<div class="flex flex-col">
<button @click="renameItem()" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
<i data-lucide="edit-2" class="w-3.5 h-3.5"></i>
<span class="text-xs font-medium">Rename</span>
</button>
<button @click="copyItem(false)" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
<i data-lucide="copy" class="w-3.5 h-3.5"></i>
<span class="text-xs font-medium">Copy</span>
</button>
<button @click="copyItem(true)" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
<i data-lucide="scissors" class="w-3.5 h-3.5"></i>
<span class="text-xs font-medium">Cut</span>
</button>
<div class="h-px bg-slate-100 my-1"></div>
<button @click="deleteItem()" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-red-600">
<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>
<span class="text-xs font-medium">Delete</span>
</button>
</div>
</template>
<template x-if="contextMenu.type === 'deadspace'">
<div class="flex flex-col">
<button @click="pasteItem()" :disabled="!clipboard.item" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700 disabled:opacity-30">
<i data-lucide="clipboard" class="w-3.5 h-3.5"></i>
<span class="text-xs font-medium">Paste</span>
</button>
<button @click="createFolder()" class="flex items-center space-x-2 px-3 py-2 hover:bg-slate-50 text-slate-700">
<i data-lucide="folder-plus" class="w-3.5 h-3.5"></i>
<span class="text-xs font-medium">New Folder</span>
</button>
</div>
</template>
</div>
</div>
<!-- Commit Modal -->
<div x-show="commitModal" x-cloak class="fixed inset-0 z-[70] flex items-center justify-center p-4">
<div @click="!isPushing && (commitModal = false)" class="fixed inset-0 bg-black/40 backdrop-blur-sm"></div>
<div class="relative bg-white rounded-3xl shadow-2xl w-full max-w-sm overflow-hidden flex flex-col max-h-[80vh]">
<div class="p-6 space-y-4 flex-1 overflow-y-auto custom-scrollbar">
<div class="flex items-center justify-between">
<h3 class="text-xl font-semibold text-slate-900">Push Changes</h3>
<span class="bg-orange-100 text-orange-600 text-[10px] font-bold px-2 py-0.5 rounded-full" x-text="`${pendingChanges.length} items`"></span>
</div>
<div class="space-y-2">
<template x-for="change in pendingChanges" :key="change.id">
<div class="flex items-center justify-between p-2 bg-slate-50 rounded-lg border border-slate-100">
<div class="flex items-center space-x-2 truncate">
<span :class="{'text-green-600': change.type === 'add', 'text-red-600': change.type === 'delete', 'text-blue-600': change.type !== 'add' && change.type !== 'delete'}" class="text-[10px] font-bold uppercase" x-text="change.type"></span>
<span class="text-xs text-slate-600 truncate" x-text="change.path"></span>
</div>
</div>
</template>
</div>
<div class="space-y-2">
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 ml-1">Commit Message</label>
<textarea x-model="commitMessage" placeholder="What did you change, Meowster?" class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all resize-none h-24 text-sm"></textarea>
</div>
</div>
<div class="p-6 bg-slate-50 border-t border-slate-100 flex flex-col space-y-3">
<button @click="pushChanges()" :disabled="isPushing" class="w-full py-3 bg-slate-900 text-white font-medium rounded-xl hover:bg-slate-800 transition-colors flex items-center justify-center space-x-2 disabled:opacity-50">
<template x-if="!isPushing">
<div class="flex items-center space-x-2">
<i data-lucide="upload-cloud" class="w-4 h-4"></i>
<span>Commit & Push</span>
</div>
</template>
<template x-if="isPushing">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
</template>
</button>
<button @click="commitModal = false" :disabled="isPushing" class="w-full py-2 text-slate-500 text-sm font-medium hover:text-slate-700 transition-colors text-center">Cancel</button>
</div>
</div>
</div>
<!-- Left Sidebar (Tree View) -->
<div x-show="leftSidebar" x-cloak class="fixed inset-0 z-[60] flex">
<div @click="leftSidebar = false" class="fixed inset-0 bg-black/20 backdrop-blur-sm"></div>
<div class="relative w-72 bg-white h-full shadow-xl flex flex-col border-r border-slate-100">
<div class="p-4 border-b border-slate-50 flex items-center justify-between">
<h2 class="text-sm font-bold text-slate-800">Explorer</h2>
<button @click="leftSidebar = false" class="p-1 hover:bg-slate-50 rounded"><i data-lucide="x" class="w-4 h-4 text-slate-400"></i></button>
</div>
<div class="flex-1 overflow-y-auto p-2 custom-scrollbar">
<template x-if="currentRepo">
<div class="space-y-0.5">
<template x-for="item in fileTree" :key="item.path">
<button @click="navigateTo(item.path); if(item.type==='blob') leftSidebar=false" class="w-full flex items-center px-2 py-1 rounded hover:bg-slate-50 text-left group" :class="currentPath === item.path ? 'bg-blue-50 text-blue-700' : 'text-slate-600'">
<div :style="`margin-left: ${(item.path.split('/').length - 1) * 12}px`" class="flex items-center">
<template x-if="item.type === 'tree'"><i data-lucide="chevron-right" class="w-3 h-3 mr-1 text-slate-300" :class="currentPath.startsWith(item.path) ? 'rotate-90' : ''"></i></template>
<template x-if="item.type === 'blob'"><span class="w-3 h-3 mr-1"></span></template>
<i :data-lucide="item.type === 'tree' ? 'folder' : 'file'" class="w-3.5 h-3.5 mr-2 opacity-70"></i>
<span class="text-[11px] truncate" x-text="item.path.split('/').pop()"></span>
</div>
</button>
</template>
</div>
</template>
</div>
</div>
</div>
<!-- Right Sidebar (Repo List) -->
<div x-show="rightSidebar" x-cloak class="fixed inset-0 z-[60] flex justify-end">
<div @click="rightSidebar = false" class="fixed inset-0 bg-black/20 backdrop-blur-sm"></div>
<div class="relative w-72 bg-white h-full shadow-xl p-4 flex flex-col">
<button @click="addRepo()" class="flex items-center justify-center space-x-2 w-full p-2.5 mb-6 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition-all shadow-lg shadow-blue-100">
<i data-lucide="plus-circle" class="w-4 h-4"></i>
<span class="text-sm font-semibold">New Repo</span>
</button>
<div class="flex-1 overflow-y-auto pr-2 space-y-2 custom-scrollbar">
<template x-for="(repo, index) in repos" :key="index">
<div class="group flex items-center justify-between p-2.5 bg-slate-50 hover:bg-slate-100 rounded-xl border border-slate-100 transition-colors cursor-pointer" @click="selectRepo(repo)">
<div class="flex items-center space-x-3 truncate">
<i data-lucide="git-fork" class="w-3.5 h-3.5 text-slate-400"></i>
<span class="text-xs font-medium text-slate-600 truncate" x-text="repo"></span>
</div>
<button @click.stop="removeRepo(index)" class="opacity-0 group-hover:opacity-100 p-1 text-slate-400 hover:text-red-500 transition-all"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i></button>
</div>
</template>
</div>
<div class="pt-4 mt-4 border-t border-slate-100">
<button @click="accountModal = true; rightSidebar = false" class="flex items-center justify-between w-full p-2.5 bg-slate-50 hover:bg-slate-100 rounded-xl transition-colors group">
<div class="flex items-center space-x-3">
<div class="bg-slate-900 rounded-full p-1.5"><i data-lucide="user" class="w-3.5 h-3.5 text-blue-400 fill-blue-400"></i></div>
<span class="text-xs font-medium text-slate-700">Account</span>
</div>
<i data-lucide="chevron-down" class="w-3.5 h-3.5 text-slate-400 group-hover:text-slate-600"></i>
</button>
</div>
</div>
</div>
<!-- Account Modal -->
<div x-show="accountModal" x-cloak class="fixed inset-0 z-[70] flex items-center justify-center p-4">
<div @click="accountModal = false" class="fixed inset-0 bg-black/40 backdrop-blur-sm"></div>
<div class="relative bg-white rounded-3xl shadow-2xl w-full max-w-sm overflow-hidden p-6 space-y-6">
<div class="space-y-2">
<h3 class="text-xl font-semibold text-slate-900">GitHub Settings</h3>
<p class="text-sm text-slate-500">Provide your Personal Access Token to enable repository access.</p>
</div>
<div class="space-y-2">
<label class="block text-xs font-bold uppercase tracking-wider text-slate-400 ml-1">Personal Access Token</label>
<input type="password" x-model="githubPat" placeholder="ghp_xxxxxxxxxxxx" class="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none transition-all">
</div>
<div class="flex flex-col space-y-3">
<button @click="localStorage.setItem('github_pat', githubPat); accountModal = false" class="w-full py-3 bg-slate-900 text-white font-medium rounded-xl hover:bg-slate-800 transition-colors">Save Changes</button>
<button @click="accountModal = false" class="w-full py-3 bg-white text-slate-500 font-medium rounded-xl hover:bg-slate-50 transition-colors">Cancel</button>
</div>
</div>
</div>
</div>
<script>lucide.createIcons();</script>
</body>
</html>