git projects - basic implementation

keyboardstaff committed Feb 2, 2026 at 23:23 UTC 360379f603e3b16d7ee003a9d785992e4807a4bf
9 files changed +633 -10
prompts/agent.system.projects.active.md
+1
@@ -2,6 +2,7 @@
2 Path: {{project_path}}
3 Title: {{project_name}}
4 Description: {{project_description}}
5 +{% if project_git_url %}Git URL: {{project_git_url}}{% endif %}
6
7
8 ### Important project instructions MUST follow
python/api/projects.py
+46
@@ -1,5 +1,6 @@
1 from python.helpers.api import ApiHandler, Input, Output, Request, Response
2 from python.helpers import projects
3 +from python.helpers.notification import NotificationManager, NotificationType, NotificationPriority
4
5
6 class Projects(ApiHandler):
@@ -17,6 +18,8 @@ class Projects(ApiHandler):
18 data = self.load_project(input.get("name", None))
19 elif action == "create":
20 data = self.create_project(input.get("project", None))
21 + elif action == "clone":
22 + data = self.clone_project(input.get("project", None))
23 elif action == "update":
24 data = self.update_project(input.get("project", None))
25 elif action == "delete":
@@ -50,6 +53,49 @@ class Projects(ApiHandler):
53 name = projects.create_project(project["name"], data)
54 return projects.load_edit_project_data(name)
55
56 + def clone_project(self, project: dict|None):
57 + if project is None:
58 + raise Exception("Project data is required")
59 + git_url = project.get("git_url", "")
60 + if not git_url:
61 + raise Exception("Git URL is required")
62 +
63 + # Progress notification
64 + notification = NotificationManager.send_notification(
65 + NotificationType.PROGRESS,
66 + NotificationPriority.NORMAL,
67 + f"Cloning repository...",
68 + "Git Clone",
69 + display_time=999,
70 + group="git_clone"
71 + )
72 +
73 + try:
74 + data = projects.BasicProjectData(**project)
75 + name = projects.clone_git_project(project["name"], git_url, data)
76 +
77 + # Success notification
78 + NotificationManager.send_notification(
79 + NotificationType.SUCCESS,
80 + NotificationPriority.NORMAL,
81 + f"Repository cloned successfully",
82 + "Git Clone",
83 + display_time=3,
84 + group="git_clone"
85 + )
86 + return projects.load_edit_project_data(name)
87 + except Exception as e:
88 + # Error notification
89 + NotificationManager.send_notification(
90 + NotificationType.ERROR,
91 + NotificationPriority.HIGH,
92 + f"Clone failed: {str(e)}",
93 + "Git Clone",
94 + display_time=5,
95 + group="git_clone"
96 + )
97 + raise
98 +
99 def load_project(self, name: str|None):
100 if name is None:
101 raise Exception("Project name is required")
python/helpers/git.py
+73 -1
@@ -54,4 +54,76 @@ def get_version():
54 git_info = get_git_info()
55 return str(git_info.get("short_tag", "")).strip() or "unknown"
56 except Exception:
57 - return "unknown"
\ No newline at end of file
57 + return "unknown"
58 +
59 +
60 +def clone_repo(url: str, dest: str, progress_callback=None):
61 + """Clone a git repository to destination."""
62 + class Progress:
63 + def __call__(self, op_code, cur_count, max_count=None, message=''):
64 + if progress_callback and max_count:
65 + progress_callback(cur_count, max_count, message)
66 + return Repo.clone_from(url, dest, progress=Progress() if progress_callback else None)
67 +
68 +
69 +# Files to ignore when checking dirty status (A0 project metadata)
70 +A0_IGNORE_PATTERNS = {".a0proj", ".a0proj/"}
71 +
72 +
73 +def get_repo_status(repo_path: str) -> dict:
74 + """Get Git repository status, ignoring A0 project metadata files."""
75 + try:
76 + repo = Repo(repo_path)
77 + if repo.bare:
78 + return {"is_git_repo": False, "error": "Repository is bare"}
79 +
80 + # Remote URL
81 + remote_url = ""
82 + try:
83 + if repo.remotes:
84 + remote_url = repo.remotes.origin.url
85 + except Exception:
86 + pass
87 +
88 + # Current branch
89 + try:
90 + current_branch = repo.active_branch.name if not repo.head.is_detached else f"HEAD@{repo.head.commit.hexsha[:7]}"
91 + except Exception:
92 + current_branch = "unknown"
93 +
94 + # Check dirty status, excluding A0 metadata
95 + def is_a0_file(path: str) -> bool:
96 + return path.startswith(".a0proj") or path == ".a0proj"
97 +
98 + # Filter out A0 files from diff and untracked
99 + changed_files = [d.a_path for d in repo.index.diff(None)] + [d.a_path for d in repo.index.diff("HEAD")]
100 + untracked = repo.untracked_files
101 +
102 + real_changes = [f for f in changed_files if not is_a0_file(f)]
103 + real_untracked = [f for f in untracked if not is_a0_file(f)]
104 +
105 + is_dirty = len(real_changes) > 0 or len(real_untracked) > 0
106 + untracked_count = len(real_untracked)
107 +
108 + last_commit = None
109 + try:
110 + commit = repo.head.commit
111 + last_commit = {
112 + "hash": commit.hexsha[:7],
113 + "message": commit.message.split('\n')[0][:80],
114 + "author": str(commit.author),
115 + "date": datetime.fromtimestamp(commit.committed_date).strftime('%Y-%m-%d %H:%M')
116 + }
117 + except Exception:
118 + pass
119 +
120 + return {
121 + "is_git_repo": True,
122 + "remote_url": remote_url,
123 + "current_branch": current_branch,
124 + "is_dirty": is_dirty,
125 + "untracked_count": untracked_count,
126 + "last_commit": last_commit
127 + }
128 + except Exception as e:
129 + return {"is_git_repo": False, "error": str(e)}
\ No newline at end of file
python/helpers/projects.py
+44 -3
@@ -33,11 +33,21 @@ class BasicProjectData(TypedDict):
33 description: str
34 instructions: str
35 color: str
36 + git_url: str
37 memory: Literal[
38 "own", "global"
39 ] # in the future we can add cutom and point to another existing folder
40 file_structure: FileStructureInjectionSettings
41
42 +class GitStatusData(TypedDict, total=False):
43 + is_git_repo: bool
44 + remote_url: str
45 + current_branch: str
46 + is_dirty: bool
47 + untracked_count: int
48 + last_commit: dict
49 + error: str
50 +
51 class EditProjectData(BasicProjectData):
52 name: str
53 instruction_files_count: int
@@ -45,6 +55,7 @@ class EditProjectData(BasicProjectData):
55 variables: str
56 secrets: str
57 subagents: dict[str, SubAgentSettings]
58 + git_status: GitStatusData
59
60
61
@@ -77,6 +88,30 @@ def create_project(name: str, data: BasicProjectData):
88 return name
89
90
91 +def clone_git_project(name: str, git_url: str, data: BasicProjectData):
92 + """Clone a git repository as a new A0 project."""
93 + from python.helpers import git
94 +
95 + abs_path = files.create_dir_safe(
96 + files.get_abs_path(PROJECTS_PARENT_DIR, name), rename_format="{name}_{number}"
97 + )
98 + actual_name = files.basename(abs_path)
99 +
100 + try:
101 + git.clone_repo(git_url, abs_path)
102 + create_project_meta_folders(actual_name)
103 + data = _normalizeBasicData(data)
104 + data["git_url"] = git_url
105 + save_project_header(actual_name, data)
106 + return actual_name
107 + except Exception as e:
108 + try:
109 + files.delete_dir(abs_path)
110 + except Exception:
111 + pass
112 + raise e
113 +
114 +
115 def load_project_header(name: str):
116 abs_path = files.get_abs_path(
117 PROJECTS_PARENT_DIR, name, PROJECT_META_DIR, PROJECT_HEADER_FILE
@@ -107,6 +142,7 @@ def _normalizeBasicData(data: BasicProjectData):
142 description=data.get("description", ""),
143 instructions=data.get("instructions", ""),
144 color=data.get("color", ""),
145 + git_url=data.get("git_url", ""),
146 memory=data.get("memory", "own"),
147 file_structure=data.get(
148 "file_structure",
@@ -123,6 +159,7 @@ def _normalizeEditData(data: EditProjectData):
159 instructions=data.get("instructions", ""),
160 variables=data.get("variables", ""),
161 color=data.get("color", ""),
162 + git_status=data.get("git_status", {"is_git_repo": False}),
163 instruction_files_count=data.get("instruction_files_count", 0),
164 knowledge_files_count=data.get("knowledge_files_count", 0),
165 secrets=data.get("secrets", ""),
@@ -169,14 +206,16 @@ def load_basic_project_data(name: str) -> BasicProjectData:
206
207
208 def load_edit_project_data(name: str) -> EditProjectData:
209 + from python.helpers import git
210 +
211 data = load_basic_project_data(name)
173 - additional_instructions = get_additional_instructions_files(
174 - name
175 - ) # for additional info
212 + additional_instructions = get_additional_instructions_files(name)
213 variables = load_project_variables(name)
214 secrets = load_project_secrets_masked(name)
215 subagents = load_project_subagents(name)
216 knowledge_files_count = get_knowledge_files_count(name)
217 + git_status = git.get_repo_status(get_project_folder(name))
218 +
219 data = EditProjectData(
220 **data,
221 name=name,
@@ -185,6 +224,7 @@ def load_edit_project_data(name: str) -> EditProjectData:
224 variables=variables,
225 secrets=secrets,
226 subagents=subagents,
227 + git_status=git_status,
228 )
229 data = _normalizeEditData(data)
230 return data
@@ -308,6 +348,7 @@ def build_system_prompt_vars(name: str):
348 "project_description": project_data.get("description", ""),
349 "project_instructions": complete_instructions or "",
350 "project_path": files.normalize_a0_path(get_project_folder(name)),
351 + "project_git_url": project_data.get("git_url", ""),
352 }
353
354
webui/components/projects/project-create.html
+52 -6
@@ -16,12 +16,33 @@
16 <x-component path="projects/project-edit-basic-data.html">
17 </x-component>
18
19 - <div class="buttons-right">
20 - <button type="button" class="button cancel"
21 - @click="$store.projects.cancelCreate()">Cancel</button>
22 - <button type="button" class="button confirm" @click="$store.projects.confirmCreate()">Create and
23 - continue</button>
24 - </div>
19 + <div class="projects-form-group">
20 + <label class="projects-form-label">Git Repository (optional)</label>
21 + <span class="projects-form-description">Clone from an existing git repository. Leave empty to create an empty project.</span>
22 + <input class="projects-form-input" type="text"
23 + x-model="$store.projects.selectedProject.git_url"
24 + :disabled="$store.projects.selectedProject._cloning"
25 + placeholder="https://github.com/user/repo.git">
26 + </div>
27 +
28 + <div class="buttons-right">
29 + <button type="button" class="button cancel"
30 + @click="$store.projects.cancelCreate()"
31 + :disabled="$store.projects.selectedProject._cloning">Cancel</button>
32 + <button type="button" class="button confirm"
33 + @click="$store.projects.confirmCreate()"
34 + :disabled="$store.projects.selectedProject._cloning">
35 + <template x-if="$store.projects.selectedProject._cloning">
36 + <span class="button-loading">
37 + <span class="spinner"></span>
38 + <span>Cloning...</span>
39 + </span>
40 + </template>
41 + <template x-if="!$store.projects.selectedProject._cloning">
42 + <span x-text="$store.projects.selectedProject.git_url ? 'Clone and continue' : 'Create and continue'"></span>
43 + </template>
44 + </button>
45 + </div>
46 </div>
47
48
@@ -41,6 +62,31 @@
62 .project-detail-header {
63 margin-bottom: 1em;
64 }
65 +
66 + .button-loading {
67 + display: inline-flex;
68 + align-items: center;
69 + gap: 0.5em;
70 + }
71 +
72 + .spinner {
73 + width: 14px;
74 + height: 14px;
75 + border: 2px solid rgba(255, 255, 255, 0.3);
76 + border-top: 2px solid white;
77 + border-radius: 50%;
78 + animation: spin 1s linear infinite;
79 + }
80 +
81 + @keyframes spin {
82 + 0% { transform: rotate(0deg); }
83 + 100% { transform: rotate(360deg); }
84 + }
85 +
86 + .button:disabled {
87 + opacity: 0.6;
88 + cursor: not-allowed;
89 + }
90 </style>
91
92 </html>
\ No newline at end of file
webui/components/projects/project-edit-basic-data.html
+191
@@ -59,6 +59,81 @@
59 </div>
60 </div>
61
62 + <!-- Git Status Section -->
63 + <template x-if="!$store.projects.selectedProject._meta.creating && $store.projects.selectedProject.git_status?.is_git_repo">
64 + <div class="projects-form-group git-status-section">
65 + <label class="projects-form-label">
66 + <span class="material-symbols-outlined git-icon">commit</span>
67 + Git Status
68 + </label>
69 + <div class="git-status-card">
70 + <!-- Repository URL -->
71 + <template x-if="$store.projects.selectedProject.git_status.remote_url">
72 + <div class="git-status-row">
73 + <span class="git-status-label">
74 + <span class="material-symbols-outlined">link</span>
75 + Repository
76 + </span>
77 + <div class="git-status-value-with-action">
78 + <span class="git-status-value git-url" x-text="$store.projects.selectedProject.git_status.remote_url"></span>
79 + <a class="git-action-link" :href="$store.projects.selectedProject.git_status.remote_url" target="_blank" rel="noopener noreferrer" title="Open in browser">
80 + <span class="material-symbols-outlined">open_in_new</span>
81 + </a>
82 + </div>
83 + </div>
84 + </template>
85 +
86 + <!-- Branch -->
87 + <div class="git-status-row">
88 + <span class="git-status-label">
89 + <span class="material-symbols-outlined">fork_right</span>
90 + Branch
91 + </span>
92 + <span class="git-status-value git-branch" x-text="$store.projects.selectedProject.git_status.current_branch"></span>
93 + </div>
94 +
95 + <!-- Status -->
96 + <div class="git-status-row">
97 + <span class="git-status-label">
98 + <span class="material-symbols-outlined">info</span>
99 + Status
100 + </span>
101 + <span class="git-status-value"
102 + :class="$store.projects.selectedProject.git_status.is_dirty ? 'git-dirty' : 'git-clean'">
103 + <template x-if="!$store.projects.selectedProject.git_status.is_dirty">
104 + <span>✓ Clean</span>
105 + </template>
106 + <template x-if="$store.projects.selectedProject.git_status.is_dirty">
107 + <span>
108 + ● Has uncommitted changes
109 + <template x-if="$store.projects.selectedProject.git_status.untracked_count > 0">
110 + <span x-text="'(' + $store.projects.selectedProject.git_status.untracked_count + ' untracked)'"></span>
111 + </template>
112 + </span>
113 + </template>
114 + </span>
115 + </div>
116 +
117 + <!-- Last Commit -->
118 + <template x-if="$store.projects.selectedProject.git_status.last_commit">
119 + <div class="git-status-row">
120 + <span class="git-status-label">
121 + <span class="material-symbols-outlined">history</span>
122 + Last Commit
123 + </span>
124 + <div class="git-commit-info">
125 + <span class="git-commit-hash" x-text="$store.projects.selectedProject.git_status.last_commit.hash"></span>
126 + <span class="git-commit-message" x-text="$store.projects.selectedProject.git_status.last_commit.message"></span>
127 + <span class="git-commit-meta">
128 + by <span x-text="$store.projects.selectedProject.git_status.last_commit.author"></span>
129 + on <span x-text="$store.projects.selectedProject.git_status.last_commit.date"></span>
130 + </span>
131 + </div>
132 + </div>
133 + </template>
134 + </div>
135 + </div>
136 + </template>
137 </div>
138
139
@@ -119,6 +194,10 @@
194 gap: 0.5em;
195 align-items: center;
196 flex-wrap: wrap;
197 + background: var(--color-input);
198 + border: 1px solid var(--color-border);
199 + border-radius: 0.5em;
200 + padding: 0.75em 1em;
201 }
202
203 .projects-color-ball {
@@ -176,6 +255,118 @@
255 }
256 }
257
258 + /* Git Status Styles */
259 + .git-status-section .projects-form-label {
260 + display: flex;
261 + align-items: center;
262 + gap: 0.5em;
263 + }
264 +
265 + .git-status-section .git-icon {
266 + font-size: 1.2em;
267 + color: var(--color-primary);
268 + }
269 +
270 + .git-status-card {
271 + background: var(--color-input);
272 + border: 1px solid var(--color-border);
273 + border-radius: 0.5em;
274 + padding: 0.75em 1em;
275 + }
276 +
277 + .git-status-row {
278 + display: flex;
279 + align-items: flex-start;
280 + gap: 1em;
281 + padding: 0.5em 0;
282 + border-bottom: 1px solid var(--color-border);
283 + }
284 +
285 + .git-status-row:last-child {
286 + border-bottom: none;
287 + }
288 +
289 + .git-status-label {
290 + display: flex;
291 + align-items: center;
292 + gap: 0.4em;
293 + min-width: 100px;
294 + font-size: 0.85em;
295 + color: var(--color-text-secondary, #888);
296 + }
297 +
298 + .git-status-label .material-symbols-outlined {
299 + font-size: 1.1em;
300 + }
301 +
302 + .git-status-value {
303 + flex: 1;
304 + font-size: 0.9em;
305 + word-break: break-all;
306 + }
307 +
308 + .git-status-value-with-action {
309 + flex: 1;
310 + display: flex;
311 + align-items: center;
312 + gap: 0.5em;
313 + }
314 +
315 + .git-action-link {
316 + color: var(--color-primary);
317 + opacity: 0.7;
318 + transition: opacity 0.15s;
319 + }
320 +
321 + .git-action-link:hover {
322 + opacity: 1;
323 + }
324 +
325 + .git-action-link .material-symbols-outlined {
326 + font-size: 1.1em;
327 + }
328 +
329 + .git-url {
330 + font-family: monospace;
331 + font-size: 0.85em;
332 + opacity: 0.9;
333 + }
334 +
335 + .git-branch {
336 + font-family: monospace;
337 + color: var(--color-primary);
338 + font-weight: 500;
339 + }
340 +
341 + .git-clean {
342 + color: #22c55e;
343 + }
344 +
345 + .git-dirty {
346 + color: #f59e0b;
347 + }
348 +
349 + .git-commit-info {
350 + display: flex;
351 + flex-direction: column;
352 + gap: 0.2em;
353 + }
354 +
355 + .git-commit-hash {
356 + font-family: monospace;
357 + color: var(--color-primary);
358 + font-size: 0.9em;
359 + }
360 +
361 + .git-commit-message {
362 + font-size: 0.9em;
363 + }
364 +
365 + .git-commit-meta {
366 + font-size: 0.8em;
367 + opacity: 0.7;
368 + }
369 +
370 </style>
371
372 </html>
\ No newline at end of file
webui/components/projects/projects-store.js
+78
@@ -5,6 +5,7 @@ import * as notifications from "/components/notifications/notification-store.js"
5 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
6 import { store as browserStore } from "/components/modals/file-browser/file-browser-store.js";
7 import * as shortcuts from "/js/shortcuts.js";
8 +import { showConfirmDialog } from "/js/confirmDialog.js";
9
10 const listModal = "projects/project-list.html";
11 const createModal = "projects/project-create.html";
@@ -90,6 +91,11 @@ const model = {
91 },
92
93 async confirmCreate() {
94 + // If git_url is provided, use clone flow
95 + if (this.selectedProject.git_url && this.selectedProject.git_url.trim()) {
96 + await this.cloneProject();
97 + return;
98 + }
99 // create folder name based on title
100 this.selectedProject.name = this._toFolderName(this.selectedProject.title);
101 const project = await this.saveSelectedProject(true);
@@ -98,6 +104,76 @@ const model = {
104 await this.openEditModal(project.name);
105 },
106
107 + async cloneProject() {
108 + // Security warning with custom dialog
109 + const confirmed = await showConfirmDialog({
110 + title: "Security Warning",
111 + message: `
112 + <p><strong>Cloning repositories from untrusted sources may pose security risks:</strong></p>
113 + <ul style="margin: 0.75em 0; padding-left: 1.5em;">
114 + <li>Malicious code execution</li>
115 + <li>Exposure of sensitive data</li>
116 + <li>System compromise</li>
117 + </ul>
118 + <p style="margin-top: 0.75em;">Only clone from sources you trust.</p>
119 + `,
120 + type: "warning",
121 + confirmText: "Clone Anyway",
122 + cancelText: "Cancel"
123 + });
124 + if (!confirmed) return;
125 +
126 + // Save reference before async operations
127 + const project = this.selectedProject;
128 + if (!project) return;
129 +
130 + // Disable button state handled by UI
131 + project._cloning = true;
132 + project.name = this._toFolderName(project.title);
133 +
134 + try {
135 + const response = await api.callJsonApi("projects", {
136 + action: "clone",
137 + project: {
138 + name: project.name,
139 + title: project.title,
140 + color: project.color,
141 + git_url: project.git_url,
142 + },
143 + });
144 +
145 + if (response?.ok) {
146 + await this.loadProjectsList();
147 + await modals.closeModal(createModal);
148 + await this.openEditModal(response.data.name);
149 + } else {
150 + notifications.toastFrontendError(
151 + response?.error || "Clone failed",
152 + "Git Clone",
153 + 5,
154 + "git_clone",
155 + notifications.NotificationPriority.NORMAL,
156 + true
157 + );
158 + }
159 + } catch (error) {
160 + console.error("Error cloning project:", error);
161 + notifications.toastFrontendError(
162 + "Error cloning project: " + error,
163 + "Git Clone",
164 + 5,
165 + "git_clone",
166 + notifications.NotificationPriority.NORMAL,
167 + true
168 + );
169 + } finally {
170 + // Use the saved reference instead of this.selectedProject
171 + if (project) {
172 + project._cloning = false;
173 + }
174 + }
175 + },
176 +
177 async confirmEdit() {
178 const project = await this.saveSelectedProject(false);
179 await this.loadProjectsList();
@@ -304,10 +380,12 @@ const model = {
380 creating: true,
381 },
382 _ownMemory: true,
383 + _cloning: false,
384 name: ``,
385 title: `Project #${this.projectList.length + 1}`,
386 description: "",
387 color: "",
388 + git_url: "",
389 };
390 },
391
webui/css/modals.css
+77
@@ -525,3 +525,80 @@ input[type="range"]::-moz-range-thumb {
525 background-position: -200% 0;
526 }
527 }
528 +
529 +/* Confirm Dialog */
530 +.confirm-dialog-backdrop {
531 + position: fixed;
532 + top: 0;
533 + left: 0;
534 + right: 0;
535 + bottom: 0;
536 + background: rgba(0, 0, 0, 0.5);
537 + display: flex;
538 + align-items: center;
539 + justify-content: center;
540 + z-index: 10000;
541 + opacity: 0;
542 + transition: opacity 0.2s ease;
543 +}
544 +
545 +.confirm-dialog-backdrop.visible {
546 + opacity: 1;
547 +}
548 +
549 +.confirm-dialog {
550 + background: var(--color-panel);
551 + border: 1px solid var(--color-border);
552 + border-radius: 8px;
553 + max-width: 450px;
554 + width: 90%;
555 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
556 + transform: scale(0.95);
557 + transition: transform 0.2s ease;
558 +}
559 +
560 +.confirm-dialog-backdrop.visible .confirm-dialog {
561 + transform: scale(1);
562 +}
563 +
564 +.confirm-dialog-header {
565 + display: flex;
566 + align-items: center;
567 + gap: 0.75em;
568 + padding: 1em 1.25em;
569 + border-bottom: 1px solid var(--color-border);
570 +}
571 +
572 +.confirm-dialog-icon {
573 + font-size: 1.5em;
574 +}
575 +
576 +.confirm-dialog-title {
577 + font-size: 1.1em;
578 + font-weight: 600;
579 + color: var(--color-text);
580 +}
581 +
582 +.confirm-dialog-body {
583 + padding: 1.25em;
584 + color: var(--color-text);
585 + line-height: 1.6;
586 + font-size: 0.95em;
587 +}
588 +
589 +.confirm-dialog-body ul {
590 + margin: 0.75em 0;
591 + padding-left: 1.5em;
592 +}
593 +
594 +.confirm-dialog-body p {
595 + margin: 0;
596 +}
597 +
598 +.confirm-dialog-footer {
599 + display: flex;
600 + justify-content: flex-end;
601 + gap: 0.75em;
602 + padding: 1em 1.25em;
603 + border-top: 1px solid var(--color-border);
604 +}
webui/js/confirmDialog.js new
+71
@@ -0,0 +1,71 @@
1 +// Custom confirmation dialog. CSS in /css/modals.css
2 +
3 +const DIALOG_TYPES = {
4 + warning: { icon: 'warning', color: 'var(--color-warning, #f59e0b)' },
5 + danger: { icon: 'error', color: 'var(--color-error, #ef4444)' },
6 + info: { icon: 'info', color: 'var(--color-primary, #3b82f6)' }
7 +};
8 +
9 +export function showConfirmDialog(options) {
10 + const {
11 + title = 'Confirm',
12 + message = '',
13 + confirmText = 'Confirm',
14 + cancelText = 'Cancel',
15 + type = 'warning'
16 + } = options;
17 +
18 + const typeConfig = DIALOG_TYPES[type] || DIALOG_TYPES.warning;
19 +
20 + return new Promise((resolve) => {
21 + // Create backdrop
22 + const backdrop = document.createElement('div');
23 + backdrop.className = 'confirm-dialog-backdrop';
24 +
25 + // Create dialog
26 + const dialog = document.createElement('div');
27 + dialog.className = 'confirm-dialog';
28 + dialog.innerHTML = `
29 + <div class="confirm-dialog-header">
30 + <span class="confirm-dialog-icon material-symbols-outlined" style="color: ${typeConfig.color}">${typeConfig.icon}</span>
31 + <span class="confirm-dialog-title">${title}</span>
32 + </div>
33 + <div class="confirm-dialog-body">${message}</div>
34 + <div class="confirm-dialog-footer">
35 + <button class="button cancel confirm-dialog-cancel">${cancelText}</button>
36 + <button class="button confirm confirm-dialog-confirm">${confirmText}</button>
37 + </div>
38 + `;
39 +
40 + backdrop.appendChild(dialog);
41 + document.body.appendChild(backdrop);
42 +
43 + // Show with animation
44 + requestAnimationFrame(() => {
45 + backdrop.classList.add('visible');
46 + dialog.querySelector('.confirm-dialog-cancel').focus();
47 + });
48 +
49 + // Close handler
50 + const close = (result) => {
51 + backdrop.classList.remove('visible');
52 + document.removeEventListener('keydown', handleKeydown);
53 + setTimeout(() => {
54 + backdrop.remove();
55 + resolve(result);
56 + }, 200);
57 + };
58 +
59 + // Event listeners
60 + dialog.querySelector('.confirm-dialog-cancel').addEventListener('click', () => close(false));
61 + dialog.querySelector('.confirm-dialog-confirm').addEventListener('click', () => close(true));
62 + backdrop.addEventListener('click', (e) => e.target === backdrop && close(false));
63 +
64 + // Keyboard handling
65 + const handleKeydown = (e) => {
66 + if (e.key === 'Escape') close(false);
67 + else if (e.key === 'Enter') close(true);
68 + };
69 + document.addEventListener('keydown', handleKeydown);
70 + });
71 +}