1322 lines
41 KiB
HTML
1322 lines
41 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fa" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>پروژه نمونه</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
|
||
/* ============ Theme Variables ============ */
|
||
:root {
|
||
--primary: #1a365d;
|
||
--primary-light: #2c5282;
|
||
--primary-dark: #0f2341;
|
||
--accent: #c9a961;
|
||
--accent-dark: #a8893f;
|
||
--accent-light: #e0c789;
|
||
--bg: #f5f7fa;
|
||
--board-bg: #e2e8f0;
|
||
--card-bg: #ffffff;
|
||
--column-bg: #edf2f7;
|
||
--panel-bg: #ffffff;
|
||
--text: #1a202c;
|
||
--text-muted: #4a5568;
|
||
--text-light: #718096;
|
||
--border: #cbd5e0;
|
||
--border-light: #e2e8f0;
|
||
--success: #38a169;
|
||
--danger: #e53e3e;
|
||
--warning: #dd6b20;
|
||
--info: #3182ce;
|
||
--shadow: 0 4px 6px rgba(0,0,0,0.08), 0 10px 20px rgba(26,54,93,0.1);
|
||
--shadow-sm: 0 2px 4px rgba(0,0,0,0.06);
|
||
--shadow-lg: 0 10px 30px rgba(0,0,0,0.15);
|
||
--radius: 10px;
|
||
--radius-sm: 6px;
|
||
--sidebar-width: 280px;
|
||
--header-height: 64px;
|
||
--tabs-height: 48px;
|
||
--footer-height: 36px;
|
||
}
|
||
[data-theme="dark"] {
|
||
--primary: #2c5282;
|
||
--primary-light: #3b6fb5;
|
||
--primary-dark: #1a365d;
|
||
--accent: #e0c789;
|
||
--accent-dark: #c9a961;
|
||
--accent-light: #f0d9a0;
|
||
--bg: #1a202c;
|
||
--board-bg: #2d3748;
|
||
--card-bg: #2d3748;
|
||
--column-bg: #374151;
|
||
--panel-bg: #1f2937;
|
||
--text: #e2e8f0;
|
||
--text-muted: #cbd5e0;
|
||
--text-light: #a0aec0;
|
||
--border: #4a5568;
|
||
--border-light: #374151;
|
||
--shadow: 0 4px 6px rgba(0,0,0,0.3), 0 10px 20px rgba(0,0,0,0.4);
|
||
--shadow-sm: 0 2px 4px rgba(0,0,0,0.2);
|
||
--shadow-lg: 0 10px 30px rgba(0,0,0,0.5);
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: 'Vazirmatn', Tahoma, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
font-size: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: background 0.3s, color 0.3s;
|
||
}
|
||
/* ============ Header ============ */
|
||
.app-header {
|
||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||
color: white;
|
||
padding: 0 20px;
|
||
height: var(--header-height);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 3px solid var(--accent);
|
||
box-shadow: var(--shadow);
|
||
z-index: 100;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.app-header::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -10%;
|
||
width: 300px;
|
||
height: 300px;
|
||
background: radial-gradient(circle, rgba(201,169,97,0.15) 0%, transparent 70%);
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
}
|
||
.app-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
.app-logo {
|
||
width: 44px; height: 44px;
|
||
background: var(--accent);
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
color: var(--primary-dark);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
|
||
flex-shrink: 0;
|
||
}
|
||
.app-brand-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
.app-title {
|
||
font-size: 16px;
|
||
font-weight: 800;
|
||
letter-spacing: -0.3px;
|
||
color: white;
|
||
}
|
||
.app-subtitle {
|
||
font-size: 11px;
|
||
opacity: 0.85;
|
||
color: rgba(255,255,255,0.9);
|
||
}
|
||
/* ============ Buttons ============ */
|
||
.btn {
|
||
background: linear-gradient(135deg, var(--primary-light), var(--primary));
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 14px;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
|
||
white-space: nowrap;
|
||
}
|
||
.btn:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); filter: brightness(1.1); }
|
||
.btn:active { transform: translateY(0); }
|
||
.btn-accent {
|
||
background: linear-gradient(135deg, var(--accent), var(--accent-dark));
|
||
color: var(--primary-dark);
|
||
font-weight: 700;
|
||
}
|
||
.btn-ghost {
|
||
background: rgba(26,54,93,0.08);
|
||
border: 1px solid var(--border);
|
||
color: var(--primary);
|
||
font-weight: 600;
|
||
}
|
||
.btn-ghost:hover {
|
||
background: rgba(26,54,93,0.15);
|
||
border-color: var(--primary-light);
|
||
}
|
||
[data-theme="dark"] .btn-ghost {
|
||
background: rgba(255,255,255,0.1);
|
||
border: 1px solid rgba(255,255,255,0.25);
|
||
color: white;
|
||
}
|
||
[data-theme="dark"] .btn-ghost:hover {
|
||
background: rgba(255,255,255,0.2);
|
||
}
|
||
/* ============ Main Layout ============ */
|
||
.app-main {
|
||
flex: 1;
|
||
display: flex;
|
||
overflow: hidden;
|
||
position: relative;
|
||
flex-direction: row;
|
||
}
|
||
/* ============ Sidebar ============ */
|
||
.sidebar {
|
||
order: -1;
|
||
width: var(--sidebar-width);
|
||
background: var(--panel-bg);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: width 0.3s ease;
|
||
box-shadow: var(--shadow);
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
z-index: 50;
|
||
}
|
||
.sidebar.collapsed {
|
||
width: 0;
|
||
overflow: hidden;
|
||
}
|
||
.sidebar-header {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: white;
|
||
padding: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 2px solid var(--accent);
|
||
min-height: 48px;
|
||
}
|
||
.sidebar-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
}
|
||
.sidebar.collapsed .sidebar-title { display: none; }
|
||
.sidebar-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 8px;
|
||
}
|
||
.sidebar.collapsed .sidebar-body { display: none; }
|
||
.sidebar-group {
|
||
margin-bottom: 6px;
|
||
border: 1px solid var(--border-light);
|
||
border-radius: var(--radius-sm);
|
||
overflow: hidden;
|
||
background: var(--card-bg);
|
||
}
|
||
.sidebar-group-header {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: white;
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
user-select: none;
|
||
transition: all 0.2s;
|
||
position: relative;
|
||
}
|
||
.sidebar-group-header:hover { filter: brightness(1.1); }
|
||
.sidebar-group-header .group-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
.sidebar-group-header .group-arrow {
|
||
transition: transform 0.3s;
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
.sidebar-group.collapsed .group-arrow { transform: rotate(90deg); }
|
||
.sidebar-group.collapsed .sidebar-group-body { display: none; }
|
||
.sidebar-group-body {
|
||
padding: 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
.sidebar-btn {
|
||
background: var(--card-bg);
|
||
border: 1px solid var(--border-light);
|
||
color: var(--text);
|
||
padding: 8px 10px;
|
||
border-radius: var(--radius-sm);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
transition: all 0.2s;
|
||
text-align: right;
|
||
width: 100%;
|
||
position: relative;
|
||
}
|
||
.sidebar-btn:hover {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: white;
|
||
border-color: var(--primary);
|
||
transform: translateX(-2px);
|
||
}
|
||
.sidebar-btn .btn-icon-emoji {
|
||
font-size: 15px;
|
||
width: 20px;
|
||
text-align: center;
|
||
flex-shrink: 0;
|
||
}
|
||
.sidebar-btn .btn-title {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.sidebar-btn .shortcut-badge {
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
padding: 2px 6px;
|
||
background: rgba(0,0,0,0.08);
|
||
color: var(--text-muted);
|
||
border-radius: 4px;
|
||
font-family: 'Vazirmatn', monospace;
|
||
flex-shrink: 0;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.sidebar-btn:hover .shortcut-badge {
|
||
background: rgba(255,255,255,0.2);
|
||
color: white;
|
||
}
|
||
/* ============ Tooltip ============ */
|
||
[data-tooltip] {
|
||
position: relative;
|
||
}
|
||
[data-tooltip]::after {
|
||
content: attr(data-tooltip);
|
||
position: absolute;
|
||
right: calc(100% + 10px);
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
background: var(--primary-dark);
|
||
color: white;
|
||
padding: 8px 12px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.2s;
|
||
z-index: 1000;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||
max-width: 250px;
|
||
white-space: normal;
|
||
line-height: 1.5;
|
||
}
|
||
[data-tooltip]::before {
|
||
content: '';
|
||
position: absolute;
|
||
right: calc(100% + 5px);
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
border: 5px solid transparent;
|
||
border-left-color: var(--primary-dark);
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.2s;
|
||
z-index: 1000;
|
||
}
|
||
[data-tooltip]:hover::after,
|
||
[data-tooltip]:hover::before {
|
||
opacity: 1;
|
||
}
|
||
[data-theme="dark"] [data-tooltip]::after {
|
||
background: var(--accent-dark);
|
||
color: var(--primary-dark);
|
||
}
|
||
[data-theme="dark"] [data-tooltip]::before {
|
||
border-left-color: var(--accent-dark);
|
||
}
|
||
/* ============ Tabs Bar ============ */
|
||
.tabs-bar {
|
||
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%);
|
||
border-bottom: 2px solid var(--accent);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 10px;
|
||
min-height: var(--tabs-height);
|
||
box-shadow: var(--shadow-sm);
|
||
flex-shrink: 0;
|
||
gap: 6px;
|
||
position: relative;
|
||
}
|
||
[data-theme="dark"] .tabs-bar {
|
||
background: linear-gradient(135deg, #0f1724 0%, #1a2a44 100%);
|
||
}
|
||
.tab-control-btn {
|
||
background: rgba(255,255,255,0.1);
|
||
border: 1px solid rgba(255,255,255,0.15);
|
||
color: white;
|
||
width: 32px; height: 32px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
padding: 0;
|
||
}
|
||
.tab-control-btn:hover:not(:disabled) {
|
||
background: var(--accent);
|
||
color: var(--primary-dark);
|
||
border-color: var(--accent);
|
||
transform: translateY(-1px);
|
||
}
|
||
/* ============ Board Area ============ */
|
||
.board-area {
|
||
flex: 1;
|
||
overflow: auto;
|
||
padding: 20px;
|
||
background: var(--board-bg);
|
||
transition: background 0.3s;
|
||
}
|
||
.board-empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: var(--text-muted);
|
||
text-align: center;
|
||
}
|
||
.board-empty-icon { font-size: 80px; opacity: 0.3; margin-bottom: 20px; }
|
||
.board-empty h2 { font-size: 20px; color: var(--primary); margin-bottom: 10px; }
|
||
[data-theme="dark"] .board-empty h2 { color: var(--accent); }
|
||
.board-empty p { margin-bottom: 20px; }
|
||
/* ============ Modals ============ */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(26,54,93,0.6);
|
||
backdrop-filter: blur(3px);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
padding: 20px;
|
||
animation: fadeIn 0.2s;
|
||
}
|
||
.modal-overlay.active { display: flex; }
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
.modal {
|
||
background: var(--panel-bg);
|
||
color: var(--text);
|
||
border-radius: 12px;
|
||
width: 100%;
|
||
max-width: 600px;
|
||
max-height: 90vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: var(--shadow-lg);
|
||
animation: slideUp 0.25s;
|
||
}
|
||
.modal-lg { max-width: 800px; }
|
||
@keyframes slideUp {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
.modal-header {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: white;
|
||
padding: 16px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
border-bottom: 3px solid var(--accent);
|
||
}
|
||
.modal-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.modal-close {
|
||
background: rgba(255,255,255,0.15);
|
||
border: none;
|
||
color: white;
|
||
width: 30px; height: 30px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
transition: all 0.2s;
|
||
}
|
||
.modal-close:hover { background: var(--danger); }
|
||
.modal-body {
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
.modal-footer {
|
||
padding: 14px 20px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
gap: 8px;
|
||
background: var(--border-light);
|
||
}
|
||
/* ============ Forms ============ */
|
||
.form-group { margin-bottom: 16px; }
|
||
.form-label {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--primary);
|
||
margin-bottom: 6px;
|
||
}
|
||
[data-theme="dark"] .form-label { color: var(--accent-light); }
|
||
.form-label .required { color: var(--danger); }
|
||
.form-input, .form-textarea, .form-select {
|
||
width: 100%;
|
||
padding: 9px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
font-family: inherit;
|
||
font-size: 13px;
|
||
background: var(--card-bg);
|
||
color: var(--text);
|
||
transition: all 0.2s;
|
||
}
|
||
.form-input:focus, .form-textarea:focus, .form-select:focus {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: 0 0 0 3px rgba(201,169,97,0.15);
|
||
}
|
||
.form-textarea { resize: vertical; min-height: 70px; line-height: 1.6; }
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
.form-hint {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-top: 4px;
|
||
}
|
||
/* ============ Webhook Section ============ */
|
||
.webhook-section {
|
||
background: var(--border-light);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
padding: 14px;
|
||
margin-top: 12px;
|
||
}
|
||
.webhook-section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
.webhook-section-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
[data-theme="dark"] .webhook-section-title { color: var(--accent-light); }
|
||
.webhook-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
.switch {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 36px;
|
||
height: 20px;
|
||
}
|
||
.switch input { opacity: 0; width: 0; height: 0; }
|
||
.slider {
|
||
position: absolute;
|
||
cursor: pointer;
|
||
inset: 0;
|
||
background: var(--border);
|
||
transition: 0.3s;
|
||
border-radius: 20px;
|
||
}
|
||
.slider::before {
|
||
position: absolute;
|
||
content: "";
|
||
height: 14px;
|
||
width: 14px;
|
||
right: 3px;
|
||
bottom: 3px;
|
||
background: white;
|
||
transition: 0.3s;
|
||
border-radius: 50%;
|
||
}
|
||
input:checked + .slider { background: var(--success); }
|
||
input:checked + .slider::before { transform: translateX(-16px); }
|
||
.webhook-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
padding: 8px 10px;
|
||
background: var(--card-bg);
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
.webhook-status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--text-muted);
|
||
flex-shrink: 0;
|
||
}
|
||
.webhook-status-dot.success { background: var(--success); }
|
||
.webhook-status-dot.error { background: var(--danger); }
|
||
.webhook-status-dot.pending { background: var(--warning); animation: pulse 1.5s infinite; }
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.4; }
|
||
}
|
||
/* ============ Toast ============ */
|
||
.toast-container {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 20px;
|
||
z-index: 2000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.toast {
|
||
background: var(--primary);
|
||
color: white;
|
||
padding: 12px 18px;
|
||
border-radius: 8px;
|
||
box-shadow: var(--shadow-lg);
|
||
font-size: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
animation: slideInLeft 0.3s;
|
||
min-width: 250px;
|
||
}
|
||
.toast.success { background: var(--success); }
|
||
.toast.error { background: var(--danger); }
|
||
.toast.warning { background: var(--warning); }
|
||
@keyframes slideInLeft {
|
||
from { opacity: 0; transform: translateX(-30px); }
|
||
to { opacity: 1; transform: translateX(0); }
|
||
}
|
||
/* ============ Footer ============ */
|
||
.app-footer {
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: white;
|
||
padding: 8px 20px;
|
||
font-size: 11px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-top: 2px solid var(--accent);
|
||
flex-shrink: 0;
|
||
}
|
||
.footer-brand { color: var(--accent); font-weight: 700; }
|
||
/* ============ Readonly Fields Style ============ */
|
||
.form-input[readonly],
|
||
.form-textarea[readonly] {
|
||
background: var(--border-light);
|
||
color: var(--text-muted);
|
||
cursor: not-allowed;
|
||
opacity: 0.85;
|
||
border-style: dashed;
|
||
}
|
||
.form-input[readonly]:focus,
|
||
.form-textarea[readonly]:focus {
|
||
border-color: var(--border);
|
||
box-shadow: none;
|
||
}
|
||
.readonly-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: var(--border-light);
|
||
border: 1px solid var(--border);
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
margin-right: 6px;
|
||
}
|
||
.readonly-badge::before {
|
||
content: '🔒';
|
||
font-size: 10px;
|
||
}
|
||
.readonly-info {
|
||
background: linear-gradient(to left, #ebf8ff, #e6f6ff);
|
||
border: 1px solid #90cdf4;
|
||
border-right: 4px solid var(--info);
|
||
padding: 10px 14px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
[data-theme="dark"] .readonly-info {
|
||
background: rgba(49, 130, 206, 0.12);
|
||
border-color: rgba(49, 130, 206, 0.3);
|
||
border-right-color: var(--info);
|
||
}
|
||
/* ============ Scrollbar ============ */
|
||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
||
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Header -->
|
||
<header class="app-header">
|
||
<div class="app-brand">
|
||
<div class="app-logo">📊</div>
|
||
<div class="app-brand-text">
|
||
<div class="app-title">پروژه نمونه</div>
|
||
<div class="app-subtitle">توضیحات پروژه</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Toolbar -->
|
||
<div class="tabs-bar">
|
||
<button class="tab-control-btn" id="sidebarToggle" title="باز/بسته کردن سایدبار">
|
||
<span id="sidebarIcon">☰</span>
|
||
</button>
|
||
<button class="tab-control-btn" id="themeToggle" title="تغییر تم">
|
||
<span id="themeIcon">🌙</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Main Layout -->
|
||
<div class="app-main">
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="sidebar-title">🛠️ ابزارها</div>
|
||
</div>
|
||
<div class="sidebar-body" id="sidebarBody"></div>
|
||
</aside>
|
||
|
||
<!-- Board Area -->
|
||
<main class="board-area">
|
||
<div class="board-empty">
|
||
<div class="board-empty-icon">📋</div>
|
||
<h2>ناحیه اصلی</h2>
|
||
<p>محتوای اصلی برنامه در این ناحیه نمایش داده میشود</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<footer class="app-footer">
|
||
<div>
|
||
<span class="footer-brand">طراح و توسعهدهنده:</span> هادی خزاعی اصل
|
||
<span style="margin: 0 10px;">|</span>
|
||
<span class="footer-brand">شرکت:</span> رایان پرداز کاوش
|
||
</div>
|
||
<div>
|
||
<span class="footer-brand">نسخه:</span> 1.0.0
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- Settings Modal -->
|
||
<div class="modal-overlay" id="settingsModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header">
|
||
<div class="modal-title">
|
||
<span>⚙️</span>
|
||
<span>تنظیمات پروژه</span>
|
||
</div>
|
||
<button class="modal-close" data-close-modal>✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="readonly-info">
|
||
<span>ℹ️</span>
|
||
<span>اطلاعات پایه پروژه (عنوان، نسخه، توضیحات، توسعهدهنده و شرکت) فقط قابل مشاهده هستند و از طریق فایل پیکربندی پروژه قابل تغییر میباشند.</span>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
عنوان پروژه
|
||
<span class="readonly-badge">فقط خواندنی</span>
|
||
</label>
|
||
<input type="text" class="form-input" id="settingProjectTitle" readonly>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
نسخه
|
||
<span class="readonly-badge">فقط خواندنی</span>
|
||
</label>
|
||
<input type="text" class="form-input" id="settingVersion" readonly>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
توضیحات
|
||
<span class="readonly-badge">فقط خواندنی</span>
|
||
</label>
|
||
<textarea class="form-textarea" id="settingDescription" readonly></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
توسعهدهنده
|
||
<span class="readonly-badge">فقط خواندنی</span>
|
||
</label>
|
||
<input type="text" class="form-input" id="settingDeveloper" readonly>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">
|
||
شرکت
|
||
<span class="readonly-badge">فقط خواندنی</span>
|
||
</label>
|
||
<input type="text" class="form-input" id="settingCompany" readonly>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="webhook-section">
|
||
<div class="webhook-section-header">
|
||
<div class="webhook-section-title">
|
||
<span>🔗</span>
|
||
<span>تنظیمات وبهوک (Webhook)</span>
|
||
</div>
|
||
<div class="webhook-toggle">
|
||
<span>فعالسازی</span>
|
||
<label class="switch">
|
||
<input type="checkbox" id="settingWebhookEnabled">
|
||
<span class="slider"></span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">آدرس URL وبهوک <span class="required">*</span></label>
|
||
<input type="url" class="form-input" id="settingWebhookUrl" dir="ltr" placeholder="https://example.com/webhook">
|
||
<div class="form-hint">آدرسی که رویدادها به آن ارسال میشوند (باید با http یا https شروع شود)</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">شناسه کلاینت (Client ID) <span class="required">*</span></label>
|
||
<input type="text" class="form-input" id="settingWebhookClientId" dir="ltr" placeholder="client-xxxx-xxxx">
|
||
<div class="form-hint">این شناسه در هدر درخواست با کلید X-Client-Id ارسال میشود</div>
|
||
</div>
|
||
|
||
<div class="webhook-status" id="webhookStatus">
|
||
<span class="webhook-status-dot" id="webhookStatusDot"></span>
|
||
<span id="webhookStatusText">وضعیت: غیرفعال</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-accent" id="saveSettingsBtn">💾 ذخیره</button>
|
||
<button class="btn btn-ghost" data-close-modal>بستن</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast Container -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<script>
|
||
// ============ Project Data Structure (New Schema) ============
|
||
const projectData = {
|
||
projectId: "550e8400-e29b-41d4-a716-446655440000",
|
||
projectName: "sample_project",
|
||
projectTitle: "پروژه نمونه",
|
||
description: "توضیحات پروژه",
|
||
version: "1.0.0",
|
||
developer: "هادی خزاعی اصل",
|
||
company: "رایان پرداز کاوش",
|
||
webhook: {
|
||
url: "",
|
||
clientId: "",
|
||
enabled: false
|
||
},
|
||
toolGroups: [
|
||
{
|
||
id: 'project-management',
|
||
title: 'پروژه',
|
||
icon: '📁',
|
||
description: 'مدیریت چرخه حیات پروژه',
|
||
tools: [
|
||
{
|
||
id: 'project-settings',
|
||
title: 'تنظیمات پروژه',
|
||
icon: '⚙️',
|
||
action: 'openSettings',
|
||
shortcut: 'Ctrl+,',
|
||
description: 'مشاهده اطلاعات پروژه و تنظیمات وبهوک',
|
||
targetEntity: 'settings'
|
||
},
|
||
{
|
||
id: 'create-project',
|
||
title: 'ایجاد پروژه جدید',
|
||
icon: '➕',
|
||
action: 'createProject',
|
||
shortcut: 'Ctrl+N',
|
||
description: 'ایجاد یک پروژه جدید با متادیتای اولیه',
|
||
targetEntity: 'projects'
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: 'data-analytics',
|
||
title: 'ابزارهای تحلیل داده',
|
||
icon: '📊',
|
||
description: 'ابزارهای تحلیل و پردازش دادهها',
|
||
tools: [
|
||
{
|
||
id: 'chart-generator',
|
||
title: 'نمودار ساز پویا',
|
||
icon: '📈',
|
||
action: 'generateChart',
|
||
shortcut: 'Ctrl+G',
|
||
description: 'ابزاری برای تبدیل جداول داده به نمودارهای میلهای و دایرهای به صورت زنده',
|
||
targetEntity: 'charts'
|
||
},
|
||
{
|
||
id: 'advanced-filter',
|
||
title: 'فیلتر پیشرفته',
|
||
icon: '🔍',
|
||
action: 'applyFilter',
|
||
shortcut: 'Ctrl+F',
|
||
description: 'امکان اعمال فیلترهای چندلایه و شرطی بر روی مجموعه دادهها',
|
||
targetEntity: 'datasets'
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: 'export-tools',
|
||
title: 'ابزارهای خروجی و گزارش',
|
||
icon: '📤',
|
||
description: 'ابزارهای تولید گزارش و خروجی',
|
||
tools: [
|
||
{
|
||
id: 'pdf-export',
|
||
title: 'خروجی PDF',
|
||
icon: '📄',
|
||
action: 'exportPDF',
|
||
shortcut: 'Ctrl+E',
|
||
description: 'تولید فایل PDF استاندارد از گزارشهای فعلی با قابلیت شخصیسازی هدر و فوتر',
|
||
targetEntity: 'reports'
|
||
}
|
||
]
|
||
}
|
||
]
|
||
};
|
||
|
||
// ============ Action Registry ============
|
||
const ActionRegistry = {
|
||
openSettings(tool, group) {
|
||
openSettingsModal();
|
||
},
|
||
createProject(tool, group) {
|
||
showToast(`🚀 اکشن "${tool.action}" اجرا شد - ${tool.title}`, 'info', 3000);
|
||
},
|
||
generateChart(tool, group) {
|
||
showToast(`📈 اکشن "${tool.action}" اجرا شد - ${tool.title}`, 'info', 3000);
|
||
},
|
||
applyFilter(tool, group) {
|
||
showToast(`🔍 اکشن "${tool.action}" اجرا شد - ${tool.title}`, 'info', 3000);
|
||
},
|
||
exportPDF(tool, group) {
|
||
showToast(`📄 اکشن "${tool.action}" اجرا شد - ${tool.title}`, 'info', 3000);
|
||
},
|
||
defaultAction(tool, group) {
|
||
showToast(`ابزار انتخاب شد: ${tool.title}`, 'info', 3000);
|
||
}
|
||
};
|
||
|
||
// ============ Toast System ============
|
||
function showToast(message, type = 'info', duration = 3000) {
|
||
const container = document.getElementById('toastContainer');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
|
||
const icons = {
|
||
success: '✅',
|
||
error: '❌',
|
||
warning: '⚠️',
|
||
info: 'ℹ️'
|
||
};
|
||
|
||
toast.innerHTML = `
|
||
<span>${icons[type] || 'ℹ️'}</span>
|
||
<span>${message}</span>
|
||
`;
|
||
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.style.transition = 'opacity 0.3s, transform 0.3s';
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateX(-30px)';
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, duration);
|
||
}
|
||
|
||
// ============ Webhook Manager ============
|
||
const WebhookManager = {
|
||
getConfig() {
|
||
return projectData.webhook;
|
||
},
|
||
|
||
setConfig(config) {
|
||
projectData.webhook = { ...projectData.webhook, ...config };
|
||
this.updateStatusUI();
|
||
},
|
||
|
||
isValid() {
|
||
const cfg = this.getConfig();
|
||
if (!cfg.enabled) return false;
|
||
if (!cfg.url || !cfg.clientId) return false;
|
||
if (!/^https?:\/\//i.test(cfg.url)) return false;
|
||
return true;
|
||
},
|
||
|
||
updateStatusUI() {
|
||
const cfg = this.getConfig();
|
||
const dot = document.getElementById('webhookStatusDot');
|
||
const text = document.getElementById('webhookStatusText');
|
||
|
||
if (!dot || !text) return;
|
||
|
||
dot.className = 'webhook-status-dot';
|
||
|
||
if (!cfg.enabled) {
|
||
text.textContent = 'وضعیت: غیرفعال';
|
||
} else if (!cfg.url || !cfg.clientId) {
|
||
dot.classList.add('pending');
|
||
text.textContent = 'وضعیت: تنظیمات ناقص';
|
||
} else if (!/^https?:\/\//i.test(cfg.url)) {
|
||
dot.classList.add('error');
|
||
text.textContent = 'وضعیت: آدرس نامعتبر';
|
||
} else {
|
||
dot.classList.add('success');
|
||
text.textContent = 'وضعیت: آماده ارسال';
|
||
}
|
||
},
|
||
|
||
async sendEvent(eventName, payload = {}) {
|
||
if (!this.isValid()) {
|
||
console.warn('[Webhook] ارسال انجام نشد - تنظیمات معتبر نیست');
|
||
return { success: false, reason: 'invalid_config' };
|
||
}
|
||
|
||
const cfg = this.getConfig();
|
||
const requestBody = {
|
||
event: eventName,
|
||
timestamp: new Date().toISOString(),
|
||
projectId: projectData.projectId,
|
||
projectName: projectData.projectName,
|
||
projectVersion: projectData.version,
|
||
clientId: cfg.clientId,
|
||
data: payload
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(cfg.url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-Client-Id': cfg.clientId,
|
||
'X-Event-Name': eventName,
|
||
'X-Project-Id': projectData.projectId
|
||
},
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
const success = response.ok;
|
||
if (success) {
|
||
console.log(`[Webhook] رویداد "${eventName}" با موفقیت ارسال شد`);
|
||
} else {
|
||
console.error(`[Webhook] خطا در ارسال: ${response.status}`);
|
||
}
|
||
return { success, status: response.status };
|
||
} catch (err) {
|
||
console.error('[Webhook] خطای شبکه:', err);
|
||
return { success: false, reason: 'network_error', error: err.message };
|
||
}
|
||
}
|
||
};
|
||
|
||
// ============ Tool Selection Handler ============
|
||
function handleToolSelect(tool, group) {
|
||
const actionHandler = ActionRegistry[tool.action] || ActionRegistry.defaultAction;
|
||
actionHandler(tool, group);
|
||
|
||
const eventPayload = {
|
||
toolId: tool.id,
|
||
toolTitle: tool.title,
|
||
toolIcon: tool.icon,
|
||
toolAction: tool.action,
|
||
toolDescription: tool.description,
|
||
targetEntity: tool.targetEntity,
|
||
groupId: group.id,
|
||
groupTitle: group.title,
|
||
selectedAt: new Date().toISOString()
|
||
};
|
||
|
||
WebhookManager.sendEvent('tool_selected', eventPayload);
|
||
}
|
||
|
||
// ============ Theme Toggle ============
|
||
const themeToggle = document.getElementById('themeToggle');
|
||
const themeIcon = document.getElementById('themeIcon');
|
||
const html = document.documentElement;
|
||
|
||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||
html.setAttribute('data-theme', savedTheme);
|
||
updateThemeUI(savedTheme);
|
||
|
||
themeToggle.addEventListener('click', () => {
|
||
const currentTheme = html.getAttribute('data-theme');
|
||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||
html.setAttribute('data-theme', newTheme);
|
||
localStorage.setItem('theme', newTheme);
|
||
updateThemeUI(newTheme);
|
||
});
|
||
|
||
function updateThemeUI(theme) {
|
||
themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
||
}
|
||
|
||
// ============ Sidebar Toggle ============
|
||
const sidebarToggle = document.getElementById('sidebarToggle');
|
||
const sidebar = document.getElementById('sidebar');
|
||
const sidebarIcon = document.getElementById('sidebarIcon');
|
||
|
||
sidebarToggle.addEventListener('click', () => {
|
||
sidebar.classList.toggle('collapsed');
|
||
sidebarIcon.textContent = sidebar.classList.contains('collapsed') ? '☰' : '✕';
|
||
});
|
||
|
||
// ============ Render Tool Groups ============
|
||
const sidebarBody = document.getElementById('sidebarBody');
|
||
|
||
function renderToolGroups() {
|
||
sidebarBody.innerHTML = '';
|
||
|
||
projectData.toolGroups.forEach(group => {
|
||
const groupElement = document.createElement('div');
|
||
groupElement.className = 'sidebar-group';
|
||
groupElement.dataset.groupId = group.id;
|
||
|
||
const headerElement = document.createElement('div');
|
||
headerElement.className = 'sidebar-group-header';
|
||
|
||
// Add tooltip if description exists
|
||
if (group.description) {
|
||
headerElement.setAttribute('data-tooltip', group.description);
|
||
}
|
||
|
||
headerElement.innerHTML = `
|
||
<div class="group-icon">
|
||
<span>${group.icon || '📁'}</span>
|
||
<span>${group.title}</span>
|
||
</div>
|
||
<span class="group-arrow">▼</span>
|
||
`;
|
||
|
||
const bodyElement = document.createElement('div');
|
||
bodyElement.className = 'sidebar-group-body';
|
||
|
||
group.tools.forEach(tool => {
|
||
const toolBtn = document.createElement('button');
|
||
toolBtn.className = 'sidebar-btn';
|
||
toolBtn.dataset.toolId = tool.id;
|
||
toolBtn.title = tool.description;
|
||
|
||
toolBtn.innerHTML = `
|
||
<span class="btn-icon-emoji">${tool.icon || '🔧'}</span>
|
||
<span class="btn-title">${tool.title}</span>
|
||
${tool.shortcut ? `<span class="shortcut-badge">${tool.shortcut}</span>` : ''}
|
||
`;
|
||
|
||
toolBtn.addEventListener('click', () => {
|
||
handleToolSelect(tool, group);
|
||
});
|
||
|
||
bodyElement.appendChild(toolBtn);
|
||
});
|
||
|
||
headerElement.addEventListener('click', () => {
|
||
groupElement.classList.toggle('collapsed');
|
||
});
|
||
|
||
groupElement.appendChild(headerElement);
|
||
groupElement.appendChild(bodyElement);
|
||
sidebarBody.appendChild(groupElement);
|
||
});
|
||
}
|
||
|
||
// ============ Settings Modal ============
|
||
const settingsModal = document.getElementById('settingsModal');
|
||
const saveSettingsBtn = document.getElementById('saveSettingsBtn');
|
||
|
||
function openSettingsModal() {
|
||
document.getElementById('settingProjectTitle').value = projectData.projectTitle;
|
||
document.getElementById('settingVersion').value = projectData.version;
|
||
document.getElementById('settingDescription').value = projectData.description;
|
||
document.getElementById('settingDeveloper').value = projectData.developer;
|
||
document.getElementById('settingCompany').value = projectData.company;
|
||
|
||
document.getElementById('settingWebhookUrl').value = projectData.webhook.url;
|
||
document.getElementById('settingWebhookClientId').value = projectData.webhook.clientId;
|
||
document.getElementById('settingWebhookEnabled').checked = projectData.webhook.enabled;
|
||
|
||
WebhookManager.updateStatusUI();
|
||
settingsModal.classList.add('active');
|
||
}
|
||
|
||
function closeSettingsModal() {
|
||
settingsModal.classList.remove('active');
|
||
}
|
||
|
||
document.querySelectorAll('[data-close-modal]').forEach(btn => {
|
||
btn.addEventListener('click', closeSettingsModal);
|
||
});
|
||
|
||
settingsModal.addEventListener('click', (e) => {
|
||
if (e.target === settingsModal) closeSettingsModal();
|
||
});
|
||
|
||
['settingProjectTitle', 'settingVersion', 'settingDescription', 'settingDeveloper', 'settingCompany'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
el.addEventListener('keydown', (e) => {
|
||
if (!e.ctrlKey && !e.metaKey && e.key.length === 1) {
|
||
e.preventDefault();
|
||
}
|
||
});
|
||
el.addEventListener('paste', (e) => e.preventDefault());
|
||
});
|
||
|
||
['settingWebhookUrl', 'settingWebhookClientId', 'settingWebhookEnabled'].forEach(id => {
|
||
document.getElementById(id).addEventListener('input', () => {
|
||
WebhookManager.setConfig({
|
||
url: document.getElementById('settingWebhookUrl').value,
|
||
clientId: document.getElementById('settingWebhookClientId').value,
|
||
enabled: document.getElementById('settingWebhookEnabled').checked
|
||
});
|
||
});
|
||
});
|
||
|
||
saveSettingsBtn.addEventListener('click', () => {
|
||
WebhookManager.setConfig({
|
||
url: document.getElementById('settingWebhookUrl').value.trim(),
|
||
clientId: document.getElementById('settingWebhookClientId').value.trim(),
|
||
enabled: document.getElementById('settingWebhookEnabled').checked
|
||
});
|
||
|
||
closeSettingsModal();
|
||
|
||
showToast('تنظیمات با موفقیت ذخیره شد', 'success');
|
||
|
||
WebhookManager.sendEvent('settings_updated', {
|
||
updatedFields: ['webhook']
|
||
});
|
||
});
|
||
|
||
// ============ Update Project UI ============
|
||
function updateProjectUI() {
|
||
document.title = projectData.projectTitle;
|
||
document.querySelector('.app-title').textContent = projectData.projectTitle;
|
||
document.querySelector('.app-subtitle').textContent = projectData.description;
|
||
document.querySelector('.app-footer').innerHTML = `
|
||
<div>
|
||
<span class="footer-brand">طراح و توسعهدهنده:</span> ${projectData.developer}
|
||
<span style="margin: 0 10px;">|</span>
|
||
<span class="footer-brand">شرکت:</span> ${projectData.company}
|
||
</div>
|
||
<div>
|
||
<span class="footer-brand">نسخه:</span> ${projectData.version}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ============ Keyboard Shortcuts ============
|
||
document.addEventListener('keydown', (e) => {
|
||
if (!(e.ctrlKey || e.metaKey)) return;
|
||
|
||
const key = e.key.toLowerCase();
|
||
|
||
for (const group of projectData.toolGroups) {
|
||
const tool = group.tools.find(t => t.shortcut &&
|
||
t.shortcut.toLowerCase().replace('ctrl+', '') === key);
|
||
if (tool) {
|
||
e.preventDefault();
|
||
handleToolSelect(tool, group);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
|
||
// ============ Initialize ============
|
||
renderToolGroups();
|
||
updateProjectUI();
|
||
WebhookManager.updateStatusUI();
|
||
|
||
setTimeout(() => {
|
||
showToast(`${projectData.projectTitle} آماده استفاده است`, 'success');
|
||
}, 500);
|
||
</script>
|
||
</body>
|
||
</html> |