add go bench client
This commit is contained in:
684
frontend/css/memory-slots.css
Normal file
684
frontend/css/memory-slots.css
Normal file
@@ -0,0 +1,684 @@
|
||||
/* Linux BenchTools - Memory Slots Visualization */
|
||||
|
||||
/* Container pour tous les slots mémoire */
|
||||
.memory-slots-container {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.memory-slots-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.memory-slots-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Style pour un slot mémoire individuel */
|
||||
.memory-slot {
|
||||
background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.memory-slot::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.memory-slot:hover {
|
||||
border-color: rgba(76, 175, 80, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.memory-slot:hover::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Slot occupé */
|
||||
.memory-slot.occupied {
|
||||
background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, var(--bg-tertiary) 100%);
|
||||
border-color: rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.memory-slot.occupied::before {
|
||||
background: var(--color-success);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.memory-slot.occupied:hover {
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
/* Slot vide */
|
||||
.memory-slot.empty {
|
||||
background: linear-gradient(135deg, rgba(158, 158, 158, 0.05) 0%, var(--bg-secondary) 100%);
|
||||
border-style: dashed;
|
||||
border-color: rgba(158, 158, 158, 0.3);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.memory-slot.empty::before {
|
||||
background: var(--text-muted);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.memory-slot.empty:hover {
|
||||
opacity: 1;
|
||||
border-color: rgba(158, 158, 158, 0.5);
|
||||
}
|
||||
|
||||
/* En-tête du slot (nom du slot) */
|
||||
.memory-slot-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.memory-slot-name {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.memory-slot.empty .memory-slot-name {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.memory-slot-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.memory-slot-status {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.memory-slot-status.occupied {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.memory-slot-status.empty {
|
||||
background: rgba(158, 158, 158, 0.2);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Corps du slot (caractéristiques) */
|
||||
.memory-slot-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.memory-slot-size {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.15rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.memory-slot.empty .memory-slot-size {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.memory-slot-spec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
.memory-slot-spec-label {
|
||||
color: var(--text-secondary);
|
||||
min-width: 85px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.memory-slot-spec-value {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.memory-slot.empty .memory-slot-spec-value {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Nouvelles classes pour affichage compact sur plusieurs lignes */
|
||||
.memory-slot-spec-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.2rem 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.memory-slot-spec-inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.memory-slot-spec-inline .memory-slot-spec-label {
|
||||
min-width: auto;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.memory-slot-spec-inline .memory-slot-spec-value {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Highlight pour la fréquence */
|
||||
.memory-slot-spec:has(.memory-slot-spec-label:contains('Fréquence')) {
|
||||
background: rgba(var(--color-primary-rgb, 33, 150, 243), 0.05);
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
/* Badge pour le type de RAM */
|
||||
.memory-type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.memory-type-badge.ddr3 {
|
||||
background: linear-gradient(135deg, #2196F3, #1976D2);
|
||||
}
|
||||
|
||||
.memory-type-badge.ddr4 {
|
||||
background: linear-gradient(135deg, #4CAF50, #388E3C);
|
||||
}
|
||||
|
||||
.memory-type-badge.ddr5 {
|
||||
background: linear-gradient(135deg, #9C27B0, #7B1FA2);
|
||||
}
|
||||
|
||||
.memory-type-badge.unknown {
|
||||
background: linear-gradient(135deg, #757575, #616161);
|
||||
}
|
||||
|
||||
/* Icône du fabricant */
|
||||
.memory-manufacturer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.memory-manufacturer-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.memory-manufacturer-name {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Légende */
|
||||
.memory-slots-legend {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.memory-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.memory-legend-indicator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.memory-legend-indicator.occupied {
|
||||
background: var(--color-success);
|
||||
border: 2px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.memory-legend-indicator.empty {
|
||||
background: transparent;
|
||||
border: 2px dashed rgba(158, 158, 158, 0.5);
|
||||
}
|
||||
|
||||
/* Memory usage bar */
|
||||
.memory-usage {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.memory-usage-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.memory-usage-title {
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.memory-bar {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.memory-bar-labels {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.memory-bar-label {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.memory-bar-label.used {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.memory-bar-label.shared {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.memory-bar-label.free {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.memory-bar-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
color: var(--bg-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.memory-bar-segment.used {
|
||||
background: var(--color-success);
|
||||
}
|
||||
|
||||
.memory-bar-segment.shared {
|
||||
background: var(--color-warning);
|
||||
}
|
||||
|
||||
.memory-bar-segment.free {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.memory-bar-legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.memory-slot-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.memory-slot-name {
|
||||
text-transform: lowercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.memory-slot-size {
|
||||
font-weight: 700;
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.memory-slot-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.memory-slot-chip {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 999px;
|
||||
padding: 0.2rem 0.6rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.memory-slot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.memory-slot-grid-label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.memory-slot-grid-value {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.memory-slot-meta {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.memory-slot-meta-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.memory-slot-meta-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.memory-slot-meta-small {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.memory-slot-tooltip {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--color-success);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem;
|
||||
width: 260px;
|
||||
max-width: 300px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(76, 175, 80, 0.2);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
z-index: 2000;
|
||||
backdrop-filter: none;
|
||||
transition: opacity 0.15s ease, visibility 0.15s ease;
|
||||
}
|
||||
|
||||
.memory-slot:hover .memory-slot-tooltip {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.2s ease 0.1s, visibility 0.2s ease 0.1s;
|
||||
}
|
||||
|
||||
.memory-slot-tooltip-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-success);
|
||||
font-size: 0.8rem;
|
||||
padding-bottom: 0.4rem;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.memory-slot-tooltip-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(min(90px, 100%), auto) 1fr;
|
||||
gap: 0.6rem;
|
||||
padding: 0.35rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.memory-slot-tooltip-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.memory-slot-tooltip-row strong {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.memory-slot-tooltip-row span {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* DMI memory details */
|
||||
.memory-dmi {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.memory-dmi-group {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.memory-dmi-title {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-info);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.memory-dmi-fields {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.memory-dmi-line {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(min(160px, 100%), min(220px, 100%)) 1fr;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.memory-dmi-line strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Tooltip content */
|
||||
.memory-tooltip-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.memory-tooltip-line {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Vue responsive */
|
||||
@media (max-width: 768px) {
|
||||
.memory-slots-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.memory-slots-legend {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.memory-bar {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.memory-bar-segment {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.memory-dmi-line {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation au chargement */
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.memory-slot {
|
||||
animation: slideInUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.memory-slot:nth-child(1) { animation-delay: 0.05s; }
|
||||
.memory-slot:nth-child(2) { animation-delay: 0.1s; }
|
||||
.memory-slot:nth-child(3) { animation-delay: 0.15s; }
|
||||
.memory-slot:nth-child(4) { animation-delay: 0.2s; }
|
||||
.memory-slot:nth-child(5) { animation-delay: 0.25s; }
|
||||
.memory-slot:nth-child(6) { animation-delay: 0.3s; }
|
||||
.memory-slot:nth-child(7) { animation-delay: 0.35s; }
|
||||
.memory-slot:nth-child(8) { animation-delay: 0.4s; }
|
||||
|
||||
/* Fixed tooltip panel on the right side of grid */
|
||||
.memory-tooltip-panel {
|
||||
position: absolute;
|
||||
right: -330px;
|
||||
top: 0;
|
||||
min-width: 280px;
|
||||
max-width: 320px;
|
||||
width: 300px;
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.memory-tooltip-panel.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.memory-tooltip-content {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.memory-tooltip-placeholder {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 2rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Wrapper for grid + panel - needs position relative for absolute panel */
|
||||
.memory-slots-container > div[style*="display: flex"] {
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Responsive: hide panel on small screens */
|
||||
@media (max-width: 1024px) {
|
||||
.memory-tooltip-panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
121
frontend/css/themes/README.md
Normal file
121
frontend/css/themes/README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Thèmes Linux BenchTools
|
||||
|
||||
Ce répertoire contient tous les thèmes de couleur disponibles pour l'application.
|
||||
|
||||
## Thèmes disponibles
|
||||
|
||||
### 🌙 Thèmes sombres
|
||||
|
||||
#### Monokai Dark (par défaut)
|
||||
- **Fichier**: `monokai-dark.css`
|
||||
- **Palette**: Classique Monokai avec tons sombres
|
||||
- **Meilleur pour**: Utilisation prolongée, environnements faiblement éclairés
|
||||
|
||||
#### Gruvbox Dark
|
||||
- **Fichier**: `gruvbox-dark.css`
|
||||
- **Palette**: Gruvbox avec tons chauds
|
||||
- **Meilleur pour**: Ambiance rétro et chaleureuse
|
||||
|
||||
### ☀️ Thèmes clairs
|
||||
|
||||
#### Monokai Light
|
||||
- **Fichier**: `monokai-light.css`
|
||||
- **Palette**: Monokai adapté pour fond clair
|
||||
- **Meilleur pour**: Environnements bien éclairés
|
||||
|
||||
#### Gruvbox Light
|
||||
- **Fichier**: `gruvbox-light.css`
|
||||
- **Palette**: Gruvbox adapté pour fond clair, tons crème
|
||||
- **Meilleur pour**: Environnements lumineux avec ambiance chaleureuse
|
||||
|
||||
## Variables CSS requises
|
||||
|
||||
Chaque thème doit définir les variables suivantes :
|
||||
|
||||
### Couleurs de fond
|
||||
- `--bg-primary`: Couleur de fond principale
|
||||
- `--bg-secondary`: Couleur de fond secondaire (cartes)
|
||||
- `--bg-tertiary`: Couleur de fond tertiaire (inputs)
|
||||
- `--bg-hover`: Couleur au survol
|
||||
|
||||
### Couleurs de texte
|
||||
- `--text-primary`: Texte principal
|
||||
- `--text-secondary`: Texte secondaire
|
||||
- `--text-muted`: Texte atténué
|
||||
|
||||
### Couleurs d'accent
|
||||
- `--color-red`: Rouge
|
||||
- `--color-orange`: Orange
|
||||
- `--color-yellow`: Jaune
|
||||
- `--color-green`: Vert
|
||||
- `--color-cyan`: Cyan
|
||||
- `--color-blue`: Bleu
|
||||
- `--color-purple`: Violet
|
||||
|
||||
### Couleurs sémantiques
|
||||
- `--color-success`: Succès (généralement vert)
|
||||
- `--color-warning`: Avertissement (généralement orange)
|
||||
- `--color-danger`: Danger (généralement rouge)
|
||||
- `--color-info`: Information (généralement bleu/cyan)
|
||||
- `--color-primary`: Couleur primaire de l'app
|
||||
|
||||
### Bordures
|
||||
- `--border-color`: Couleur de bordure normale
|
||||
- `--border-highlight`: Couleur de bordure accentuée
|
||||
|
||||
### Ombres
|
||||
- `--shadow-sm`: Petite ombre
|
||||
- `--shadow-md`: Ombre moyenne
|
||||
- `--shadow-lg`: Grande ombre
|
||||
|
||||
## Ajouter un nouveau thème
|
||||
|
||||
1. Créez un fichier `mon-theme.css` dans ce répertoire
|
||||
2. Définissez toutes les variables requises ci-dessus
|
||||
3. Ajoutez le thème dans `theme-manager.js`
|
||||
4. Ajoutez l'option dans `settings.html`
|
||||
|
||||
Exemple minimal :
|
||||
|
||||
```css
|
||||
/**
|
||||
* Mon Nouveau Thème
|
||||
*/
|
||||
|
||||
:root {
|
||||
--bg-primary: #...;
|
||||
--bg-secondary: #...;
|
||||
--bg-tertiary: #...;
|
||||
--bg-hover: #...;
|
||||
|
||||
--text-primary: #...;
|
||||
--text-secondary: #...;
|
||||
--text-muted: #...;
|
||||
|
||||
--color-red: #...;
|
||||
--color-orange: #...;
|
||||
--color-yellow: #...;
|
||||
--color-green: #...;
|
||||
--color-cyan: #...;
|
||||
--color-blue: #...;
|
||||
--color-purple: #...;
|
||||
|
||||
--color-success: #...;
|
||||
--color-warning: #...;
|
||||
--color-danger: #...;
|
||||
--color-info: #...;
|
||||
--color-primary: #...;
|
||||
|
||||
--border-color: #...;
|
||||
--border-highlight: #...;
|
||||
|
||||
--shadow-sm: 0 2px 4px rgba(...);
|
||||
--shadow-md: 0 4px 12px rgba(...);
|
||||
--shadow-lg: 0 8px 24px rgba(...);
|
||||
}
|
||||
```
|
||||
|
||||
## Aperçu
|
||||
|
||||
Pour voir un aperçu de tous les thèmes, ouvrez :
|
||||
`http://localhost:8087/theme-preview.html`
|
||||
42
frontend/css/themes/gruvbox-dark.css
Normal file
42
frontend/css/themes/gruvbox-dark.css
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Linux BenchTools - Gruvbox Dark Theme
|
||||
* Dark variant of Gruvbox color palette
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Background Colors */
|
||||
--bg-primary: #282828;
|
||||
--bg-secondary: #3c3836;
|
||||
--bg-tertiary: #504945;
|
||||
--bg-hover: #665c54;
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #ebdbb2;
|
||||
--text-secondary: #d5c4a1;
|
||||
--text-muted: #a89984;
|
||||
|
||||
/* Gruvbox Accent Colors */
|
||||
--color-red: #fb4934;
|
||||
--color-orange: #fe8019;
|
||||
--color-yellow: #fabd2f;
|
||||
--color-green: #b8bb26;
|
||||
--color-cyan: #8ec07c;
|
||||
--color-blue: #83a598;
|
||||
--color-purple: #d3869b;
|
||||
|
||||
/* Semantic Colors */
|
||||
--color-success: #b8bb26;
|
||||
--color-warning: #fabd2f;
|
||||
--color-danger: #fb4934;
|
||||
--color-info: #83a598;
|
||||
--color-primary: #b8bb26;
|
||||
|
||||
/* Borders */
|
||||
--border-color: #504945;
|
||||
--border-highlight: #83a598;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
42
frontend/css/themes/gruvbox-light.css
Normal file
42
frontend/css/themes/gruvbox-light.css
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Linux BenchTools - Gruvbox Light Theme
|
||||
* Light variant of Gruvbox color palette
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Background Colors */
|
||||
--bg-primary: #fbf1c7;
|
||||
--bg-secondary: #f9f5d7;
|
||||
--bg-tertiary: #ebdbb2;
|
||||
--bg-hover: #d5c4a1;
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #3c3836;
|
||||
--text-secondary: #504945;
|
||||
--text-muted: #7c6f64;
|
||||
|
||||
/* Gruvbox Accent Colors (adjusted for light background) */
|
||||
--color-red: #cc241d;
|
||||
--color-orange: #d65d0e;
|
||||
--color-yellow: #d79921;
|
||||
--color-green: #98971a;
|
||||
--color-cyan: #689d6a;
|
||||
--color-blue: #458588;
|
||||
--color-purple: #b16286;
|
||||
|
||||
/* Semantic Colors */
|
||||
--color-success: #98971a;
|
||||
--color-warning: #d79921;
|
||||
--color-danger: #cc241d;
|
||||
--color-info: #458588;
|
||||
--color-primary: #98971a;
|
||||
|
||||
/* Borders */
|
||||
--border-color: #d5c4a1;
|
||||
--border-highlight: #458588;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
42
frontend/css/themes/mix-monokai-gruvbox.css
Normal file
42
frontend/css/themes/mix-monokai-gruvbox.css
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Linux BenchTools - Mix Monokai-Gruvbox Theme
|
||||
* Thème hybride : arrière-plans Monokai + couleurs d'accent Gruvbox
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Background Colors - Monokai */
|
||||
--bg-primary: #1e1e1e;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-tertiary: #3e3e3e;
|
||||
--bg-hover: #4e4e4e;
|
||||
|
||||
/* Text Colors - Gruvbox */
|
||||
--text-primary: #ebdbb2;
|
||||
--text-secondary: #d5c4a1;
|
||||
--text-muted: #a89984;
|
||||
|
||||
/* Gruvbox Accent Colors */
|
||||
--color-red: #fb4934;
|
||||
--color-orange: #fe8019;
|
||||
--color-yellow: #fabd2f;
|
||||
--color-green: #b8bb26;
|
||||
--color-cyan: #8ec07c;
|
||||
--color-blue: #83a598;
|
||||
--color-purple: #d3869b;
|
||||
|
||||
/* Semantic Colors - Gruvbox */
|
||||
--color-success: #b8bb26;
|
||||
--color-warning: #fabd2f;
|
||||
--color-danger: #fb4934;
|
||||
--color-info: #83a598;
|
||||
--color-primary: #b8bb26;
|
||||
|
||||
/* Borders - Mix */
|
||||
--border-color: #504945;
|
||||
--border-highlight: #83a598;
|
||||
|
||||
/* Shadows - Monokai (dark) */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
42
frontend/css/themes/monokai-dark.css
Normal file
42
frontend/css/themes/monokai-dark.css
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Linux BenchTools - Monokai Dark Theme
|
||||
* Default theme with dark Monokai color palette
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Background Colors */
|
||||
--bg-primary: #1e1e1e;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-tertiary: #3e3e3e;
|
||||
--bg-hover: #4e4e4e;
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #f8f8f2;
|
||||
--text-secondary: #cccccc;
|
||||
--text-muted: #75715e;
|
||||
|
||||
/* Monokai Accent Colors */
|
||||
--color-red: #f92672;
|
||||
--color-orange: #fd971f;
|
||||
--color-yellow: #e6db74;
|
||||
--color-green: #a6e22e;
|
||||
--color-cyan: #66d9ef;
|
||||
--color-blue: #66d9ef;
|
||||
--color-purple: #ae81ff;
|
||||
|
||||
/* Semantic Colors */
|
||||
--color-success: #a6e22e;
|
||||
--color-warning: #fd971f;
|
||||
--color-danger: #f92672;
|
||||
--color-info: #66d9ef;
|
||||
--color-primary: #a6e22e;
|
||||
|
||||
/* Borders */
|
||||
--border-color: #444444;
|
||||
--border-highlight: #66d9ef;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
42
frontend/css/themes/monokai-light.css
Normal file
42
frontend/css/themes/monokai-light.css
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Linux BenchTools - Monokai Light Theme
|
||||
* Light variant of Monokai theme
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Background Colors */
|
||||
--bg-primary: #f9f9f9;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #e8e8e8;
|
||||
--bg-hover: #d8d8d8;
|
||||
|
||||
/* Text Colors */
|
||||
--text-primary: #272822;
|
||||
--text-secondary: #555555;
|
||||
--text-muted: #999999;
|
||||
|
||||
/* Monokai Accent Colors (adjusted for light background) */
|
||||
--color-red: #d81857;
|
||||
--color-orange: #d87b18;
|
||||
--color-yellow: #b8a900;
|
||||
--color-green: #7cb82f;
|
||||
--color-cyan: #0099cc;
|
||||
--color-blue: #0099cc;
|
||||
--color-purple: #8b5fd8;
|
||||
|
||||
/* Semantic Colors */
|
||||
--color-success: #7cb82f;
|
||||
--color-warning: #d87b18;
|
||||
--color-danger: #d81857;
|
||||
--color-info: #0099cc;
|
||||
--color-primary: #7cb82f;
|
||||
|
||||
/* Borders */
|
||||
--border-color: #d0d0d0;
|
||||
--border-highlight: #0099cc;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
33
frontend/css/variables.css
Normal file
33
frontend/css/variables.css
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Linux BenchTools - CSS Variables communes
|
||||
* Variables de layout qui ne changent pas selon le thème
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
|
||||
/* Border Radius */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 0.15s ease;
|
||||
--transition-normal: 0.2s ease;
|
||||
--transition-slow: 0.3s ease;
|
||||
|
||||
/* Font */
|
||||
--font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
--font-mono: 'Courier New', Courier, monospace;
|
||||
|
||||
/* Icon sizing (customisable par l'utilisateur) */
|
||||
--section-icon-size: 32px;
|
||||
--button-icon-size: 24px;
|
||||
--icon-btn-size: 42px;
|
||||
--icon-btn-icon-size: 26px;
|
||||
}
|
||||
@@ -222,6 +222,9 @@
|
||||
<script src="config.js"></script>
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/icon-manager.js"></script>
|
||||
<script src="js/theme-manager.js"></script>
|
||||
<script src="js/hardware-renderer.js"></script>
|
||||
<script src="js/device_detail.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -80,6 +80,9 @@
|
||||
<script src="config.js"></script>
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/icon-manager.js"></script>
|
||||
<script src="js/theme-manager.js"></script>
|
||||
<script src="js/hardware-renderer.js"></script>
|
||||
<script src="js/devices.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -91,36 +91,8 @@ function renderDeviceHeader() {
|
||||
function renderMotherboardDetails() {
|
||||
const snapshot = currentDevice.last_hardware_snapshot;
|
||||
const container = document.getElementById('motherboardDetails');
|
||||
|
||||
if (!snapshot) {
|
||||
container.innerHTML = '<p style="color: var(--text-muted);">Aucune information disponible</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to clean empty/whitespace-only strings
|
||||
const cleanValue = (val) => {
|
||||
if (!val || (typeof val === 'string' && val.trim() === '')) return 'N/A';
|
||||
return val;
|
||||
};
|
||||
|
||||
const items = [
|
||||
{ label: 'Fabricant', value: cleanValue(snapshot.motherboard_vendor) },
|
||||
{ label: 'Modèle', value: cleanValue(snapshot.motherboard_model) },
|
||||
{ label: 'Version BIOS', value: cleanValue(snapshot.bios_version) },
|
||||
{ label: 'Date BIOS', value: cleanValue(snapshot.bios_date) },
|
||||
{ label: 'Slots RAM', value: `${snapshot.ram_slots_used || '?'} utilisés / ${snapshot.ram_slots_total || '?'} total` }
|
||||
];
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
|
||||
${items.map(item => `
|
||||
<div class="hardware-item">
|
||||
<div class="hardware-item-label">${item.label}</div>
|
||||
<div class="hardware-item-value">${escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
if (!container) return;
|
||||
container.innerHTML = HardwareRenderer.renderMotherboardDetails(snapshot);
|
||||
}
|
||||
|
||||
// Render CPU Details
|
||||
@@ -279,7 +251,10 @@ function renderStorageDetails() {
|
||||
html += '<div style="display: grid; gap: 1rem;">';
|
||||
|
||||
devices.forEach(disk => {
|
||||
const typeIcon = disk.type === 'SSD' ? '💾' : '💿';
|
||||
const diskType = (disk.type || '').toString().toLowerCase();
|
||||
const diskInterface = (disk.interface || '').toString().toLowerCase();
|
||||
const isSsd = diskType === 'ssd' || diskType === 'nvme' || diskInterface === 'nvme';
|
||||
const typeIcon = isSsd ? '💾' : '💿';
|
||||
const healthColor = disk.smart_health === 'PASSED' ? 'var(--color-success)' :
|
||||
disk.smart_health === 'FAILED' ? 'var(--color-danger)' :
|
||||
'var(--text-secondary)';
|
||||
@@ -318,7 +293,7 @@ function renderStorageDetails() {
|
||||
<strong>Interface:</strong> ${escapeHtml(disk.interface)}
|
||||
</div>
|
||||
` : ''}
|
||||
${disk.temperature_c ? `
|
||||
${(disk.temperature_c !== null && disk.temperature_c !== undefined) ? `
|
||||
<div>
|
||||
<strong>Température:</strong> ${disk.temperature_c}°C
|
||||
</div>
|
||||
|
||||
@@ -14,33 +14,34 @@ let editingNotes = false;
|
||||
let editingUpgradeNotes = false;
|
||||
let editingPurchase = false;
|
||||
|
||||
const SECTION_ICON_PATHS = {
|
||||
motherboard: 'icons/icons8-motherboard-94.png',
|
||||
cpu: 'icons/icons8-processor-94.png',
|
||||
ram: 'icons/icons8-memory-slot-94.png',
|
||||
storage: 'icons/icons8-ssd-94.png',
|
||||
gpu: 'icons/icons8-gpu-64.png',
|
||||
network: 'icons/icons8-network-cable-94.png',
|
||||
usb: 'icons/icons8-usb-memory-stick-94.png',
|
||||
pci: 'icons/icons8-pcie-48.png',
|
||||
os: 'icons/icons8-operating-system-64.png',
|
||||
shares: 'icons/icons8-shared-folder-94.png',
|
||||
benchmarks: 'icons/icons8-benchmark-64.png',
|
||||
metadata: 'icons/icons8-hardware-64.png',
|
||||
images: 'icons/icons8-picture-48.png',
|
||||
pdf: 'icons/icons8-bios-94.png',
|
||||
links: 'icons/icons8-server-94.png',
|
||||
tags: 'icons/icons8-check-mark-48.png',
|
||||
notes: 'icons/icons8-edit-pencil-48.png',
|
||||
purchase: 'icons/icons8-laptop-50.png',
|
||||
upgrade: 'icons/icons8-workstation-94.png'
|
||||
// Section icon mapping - uses data-icon with IconManager
|
||||
const SECTION_ICON_NAMES = {
|
||||
motherboard: 'motherboard',
|
||||
cpu: 'cpu',
|
||||
ram: 'memory',
|
||||
storage: 'hdd',
|
||||
gpu: 'gpu',
|
||||
network: 'network',
|
||||
usb: 'usb',
|
||||
pci: 'pci',
|
||||
os: 'desktop',
|
||||
shares: 'folder',
|
||||
benchmarks: 'chart-line',
|
||||
metadata: 'info-circle',
|
||||
images: 'image',
|
||||
pdf: 'file-pdf',
|
||||
links: 'link',
|
||||
tags: 'tag',
|
||||
notes: 'edit',
|
||||
purchase: 'shopping-cart',
|
||||
upgrade: 'rocket'
|
||||
};
|
||||
|
||||
function getSectionIcon(key, altText) {
|
||||
const src = SECTION_ICON_PATHS[key];
|
||||
if (!src) return '';
|
||||
const iconName = SECTION_ICON_NAMES[key];
|
||||
if (!iconName) return '';
|
||||
const safeAlt = utils.escapeHtml(altText || key);
|
||||
return `<img src="${src}" alt="${safeAlt}" class="section-icon" loading="lazy">`;
|
||||
return `<span class="section-icon" data-icon="${iconName}" title="${safeAlt}"></span>`;
|
||||
}
|
||||
|
||||
// Load devices
|
||||
@@ -1083,6 +1084,117 @@ async function viewBenchmarkDetails(benchmarkId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Render IP Display with edit capability
|
||||
function renderIPDisplay(snapshot, device) {
|
||||
// Extract non-loopback IPs
|
||||
const networkInterfaces = snapshot?.network_interfaces_json ?
|
||||
(typeof snapshot.network_interfaces_json === 'string' ? JSON.parse(snapshot.network_interfaces_json) : snapshot.network_interfaces_json) :
|
||||
[];
|
||||
|
||||
const ips = networkInterfaces
|
||||
.filter(iface => iface.ipv4 && iface.ipv4 !== '127.0.0.1' && iface.ipv4 !== 'N/A')
|
||||
.map(iface => iface.ipv4);
|
||||
|
||||
const displayIP = ips.length > 0 ? ips.join(', ') : 'N/A';
|
||||
const ipUrl = device.ip_url || (ips.length > 0 ? `http://${ips[0]}` : '');
|
||||
|
||||
return `
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
${ipUrl ? `<a href="${utils.escapeHtml(ipUrl)}" target="_blank" rel="noopener noreferrer" style="color: var(--color-info); text-decoration: none; font-weight: 600;" title="Ouvrir ${utils.escapeHtml(ipUrl)}">${utils.escapeHtml(displayIP)}</a>` : `<span>${utils.escapeHtml(displayIP)}</span>`}
|
||||
<button id="btn-edit-ip-url" class="icon-btn" data-icon="edit" title="Éditer le lien IP" type="button" style="padding: 0.25rem; font-size: 0.75rem;">
|
||||
<span data-icon="edit"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="ip-url-editor" style="display: none;">
|
||||
<input type="text" id="ip-url-input" class="ip-url-input" placeholder="http://${ips[0] || '10.0.0.1'}" value="${utils.escapeHtml(ipUrl)}" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary);">
|
||||
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<button id="btn-save-ip-url" class="btn btn-success btn-sm" data-icon="check" type="button">
|
||||
<span data-icon="check"></span> Sauvegarder
|
||||
</button>
|
||||
<button id="btn-cancel-ip-url" class="btn btn-secondary btn-sm" data-icon="times" type="button">
|
||||
<span data-icon="times"></span> Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function editIPUrl() {
|
||||
const editor = document.getElementById('ip-url-editor');
|
||||
const btnEdit = document.getElementById('btn-edit-ip-url');
|
||||
if (!editor || !btnEdit) return;
|
||||
|
||||
editor.style.display = 'block';
|
||||
btnEdit.style.display = 'none';
|
||||
document.getElementById('ip-url-input')?.focus();
|
||||
}
|
||||
|
||||
async function saveIPUrl() {
|
||||
if (!currentDevice) return;
|
||||
|
||||
const input = document.getElementById('ip-url-input');
|
||||
if (!input) return;
|
||||
|
||||
let url = input.value.trim();
|
||||
|
||||
// Auto-prefix http:// if not present and not empty
|
||||
if (url && !url.match(/^https?:\/\//)) {
|
||||
url = `http://${url}`;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.updateDevice(currentDevice.id, { ip_url: url || null });
|
||||
utils.showToast('Lien IP sauvegardé', 'success');
|
||||
await reloadCurrentDevice();
|
||||
} catch (error) {
|
||||
console.error('Failed to save IP URL:', error);
|
||||
utils.showToast(error.message || 'Échec de la sauvegarde du lien IP', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelIPUrlEdit() {
|
||||
if (!currentDevice) return;
|
||||
|
||||
const editor = document.getElementById('ip-url-editor');
|
||||
const btnEdit = document.getElementById('btn-edit-ip-url');
|
||||
if (!editor || !btnEdit) return;
|
||||
|
||||
editor.style.display = 'none';
|
||||
btnEdit.style.display = 'inline-block';
|
||||
|
||||
// Reset input value
|
||||
const input = document.getElementById('ip-url-input');
|
||||
if (input) {
|
||||
input.value = currentDevice.ip_url || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Search model on web
|
||||
function searchModelOnWeb() {
|
||||
const btn = document.getElementById('btn-search-model');
|
||||
if (!btn) return;
|
||||
|
||||
const model = btn.dataset.model;
|
||||
if (!model || model === 'N/A') {
|
||||
utils.showToast('Aucun modèle à rechercher', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get search engine from settings (default: Google)
|
||||
const searchEngine = localStorage.getItem('searchEngine') || 'google';
|
||||
|
||||
const searchUrls = {
|
||||
google: `https://www.google.com/search?q=${encodeURIComponent(model)}`,
|
||||
duckduckgo: `https://duckduckgo.com/?q=${encodeURIComponent(model)}`,
|
||||
bing: `https://www.bing.com/search?q=${encodeURIComponent(model)}`
|
||||
};
|
||||
|
||||
const url = searchUrls[searchEngine] || searchUrls.google;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
// Render device details (right panel)
|
||||
function renderDeviceDetails(device) {
|
||||
const previousDeviceId = currentDevice?.id;
|
||||
@@ -1118,6 +1230,14 @@ function renderDeviceDetails(device) {
|
||||
<div class="header-meta">${metaParts.length > 0 ? metaParts.join(' • ') : 'Aucune métadonnée'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<div class="header-label">Adresse IP</div>
|
||||
<div class="header-value" id="ip-display-container">
|
||||
${renderIPDisplay(snapshot, device)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<div class="header-label">Marque</div>
|
||||
@@ -1127,7 +1247,12 @@ function renderDeviceDetails(device) {
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<div class="header-label">Modèle</div>
|
||||
<div class="header-value">${utils.escapeHtml(model)}</div>
|
||||
<div class="header-value" style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>${utils.escapeHtml(model)}</span>
|
||||
<button id="btn-search-model" class="icon-btn" title="Recherche sur le Web" type="button" style="padding: 0.25rem; font-size: 0.75rem;" data-model="${utils.escapeHtml(model)}">
|
||||
<span data-icon="globe"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-row">
|
||||
@@ -1291,6 +1416,11 @@ function renderDeviceDetails(device) {
|
||||
|
||||
detailsContainer.innerHTML = headerHtml + orderedSections;
|
||||
|
||||
// Initialize icons using IconManager
|
||||
if (window.IconManager) {
|
||||
window.IconManager.inlineSvgIcons(detailsContainer);
|
||||
}
|
||||
|
||||
bindDetailActions();
|
||||
loadLinksSection(device.id);
|
||||
loadBenchmarkHistorySection(device.id);
|
||||
@@ -1305,6 +1435,14 @@ function bindDetailActions() {
|
||||
const btnUploadPDF = document.getElementById('btn-upload-pdf');
|
||||
const btnDelete = document.getElementById('btn-delete');
|
||||
|
||||
// IP URL editing
|
||||
const btnEditIpUrl = document.getElementById('btn-edit-ip-url');
|
||||
const btnSaveIpUrl = document.getElementById('btn-save-ip-url');
|
||||
const btnCancelIpUrl = document.getElementById('btn-cancel-ip-url');
|
||||
|
||||
// Web search
|
||||
const btnSearchModel = document.getElementById('btn-search-model');
|
||||
|
||||
if (btnEdit) btnEdit.addEventListener('click', toggleEditMode);
|
||||
if (btnSave) btnSave.addEventListener('click', saveDevice);
|
||||
if (btnCancel) {
|
||||
@@ -1317,6 +1455,14 @@ function bindDetailActions() {
|
||||
if (btnUploadImageHeader) btnUploadImageHeader.addEventListener('click', uploadImage);
|
||||
if (btnUploadPDF) btnUploadPDF.addEventListener('click', uploadPDF);
|
||||
if (btnDelete) btnDelete.addEventListener('click', deleteCurrentDevice);
|
||||
|
||||
// Bind IP URL actions
|
||||
if (btnEditIpUrl) btnEditIpUrl.addEventListener('click', editIPUrl);
|
||||
if (btnSaveIpUrl) btnSaveIpUrl.addEventListener('click', saveIPUrl);
|
||||
if (btnCancelIpUrl) btnCancelIpUrl.addEventListener('click', cancelIPUrlEdit);
|
||||
|
||||
// Bind web search
|
||||
if (btnSearchModel) btnSearchModel.addEventListener('click', searchModelOnWeb);
|
||||
}
|
||||
|
||||
async function loadLinksSection(deviceId) {
|
||||
|
||||
579
frontend/js/hardware-renderer.js
Normal file
579
frontend/js/hardware-renderer.js
Normal file
@@ -0,0 +1,579 @@
|
||||
// Hardware Renderer - Common rendering functions for hardware sections
|
||||
// Shared between devices.js and device_detail.js to avoid duplication
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Get utilities from global scope
|
||||
const utils = window.BenchUtils;
|
||||
|
||||
// Helper: Clean empty/whitespace values
|
||||
const cleanValue = (val) => {
|
||||
if (!val || (typeof val === 'string' && val.trim() === '')) return 'N/A';
|
||||
return val;
|
||||
};
|
||||
|
||||
// Helper: Render no data message
|
||||
const noData = (message = 'Aucune information disponible') => {
|
||||
return `<p style="color: var(--text-muted); text-align: center; padding: 2rem;">${message}</p>`;
|
||||
};
|
||||
|
||||
// =======================
|
||||
// MOTHERBOARD SECTION
|
||||
// =======================
|
||||
function renderMotherboardDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
const items = [
|
||||
{ label: 'Fabricant', value: cleanValue(snapshot.motherboard_vendor || snapshot.system_vendor) },
|
||||
{ label: 'Modèle', value: cleanValue(snapshot.motherboard_model || snapshot.system_model) },
|
||||
{ label: 'BIOS fabricant', value: cleanValue(snapshot.bios_vendor) },
|
||||
{ label: 'Version BIOS', value: cleanValue(snapshot.bios_version) },
|
||||
{ label: 'Date BIOS', value: cleanValue(snapshot.bios_date) },
|
||||
{ label: 'Hostname', value: cleanValue(snapshot.hostname) },
|
||||
{ label: 'Slots RAM', value: (snapshot.ram_slots_used != null || snapshot.ram_slots_total != null) ? `${snapshot.ram_slots_used ?? '?'} / ${snapshot.ram_slots_total ?? '?'}` : 'N/A' },
|
||||
{ label: 'Famille', value: cleanValue(snapshot.system_family) },
|
||||
{ label: 'Châssis', value: cleanValue(snapshot.chassis_type) }
|
||||
];
|
||||
|
||||
return `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
|
||||
${items.map(item => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// CPU SECTION
|
||||
// =======================
|
||||
function renderCPUDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
// Parse multi-CPU from raw_info if available
|
||||
const rawInfo = snapshot.raw_info_json ? (typeof snapshot.raw_info_json === 'string' ? JSON.parse(snapshot.raw_info_json) : snapshot.raw_info_json) : null;
|
||||
const dmidecode = rawInfo?.dmidecode || '';
|
||||
|
||||
// Check for multi-socket CPUs (Proc 1, Proc 2, etc.)
|
||||
const cpuSockets = [];
|
||||
const socketRegex = /Handle 0x[0-9A-F]+, DMI type 4[\s\S]*?Socket Designation: (.*?)[\s\S]*?Version: (.*?)[\s\S]*?Core Count: (\d+)[\s\S]*?Thread Count: (\d+)[\s\S]*?(?:Max Speed: (\d+) MHz)?[\s\S]*?(?:Current Speed: (\d+) MHz)?[\s\S]*?(?:Voltage: ([\d.]+ V))?/g;
|
||||
|
||||
let match;
|
||||
while ((match = socketRegex.exec(dmidecode)) !== null) {
|
||||
cpuSockets.push({
|
||||
socket: match[1].trim(),
|
||||
model: match[2].trim(),
|
||||
cores: match[3],
|
||||
threads: match[4],
|
||||
maxSpeed: match[5] ? `${match[5]} MHz` : 'N/A',
|
||||
currentSpeed: match[6] ? `${match[6]} MHz` : 'N/A',
|
||||
voltage: match[7] || 'N/A'
|
||||
});
|
||||
}
|
||||
|
||||
// Parse CPU signature (Family, Model, Stepping)
|
||||
const signatureRegex = /Signature: Type \d+, Family (\d+), Model (\d+), Stepping (\d+)/;
|
||||
const sigMatch = dmidecode.match(signatureRegex);
|
||||
const cpuSignature = sigMatch ? `Family ${sigMatch[1]}, Model ${sigMatch[2]}, Stepping ${sigMatch[3]}` : 'N/A';
|
||||
|
||||
const items = [
|
||||
{ label: 'Fabricant', value: snapshot.cpu_vendor || 'N/A', tooltip: snapshot.cpu_model },
|
||||
{ label: 'Modèle', value: snapshot.cpu_model || 'N/A', tooltip: snapshot.cpu_microarchitecture ? `Architecture: ${snapshot.cpu_microarchitecture}` : null },
|
||||
{ label: 'Signature CPU', value: cpuSignature },
|
||||
{ label: 'Socket', value: cpuSockets.length > 0 ? cpuSockets[0].socket : 'N/A' },
|
||||
{ label: 'Famille', value: snapshot.cpu_vendor || 'N/A' },
|
||||
{ label: 'Microarchitecture', value: snapshot.cpu_microarchitecture || 'N/A' },
|
||||
{ label: 'Cores', value: snapshot.cpu_cores != null ? snapshot.cpu_cores : 'N/A', tooltip: snapshot.cpu_threads ? `${snapshot.cpu_threads} threads disponibles` : null },
|
||||
{ label: 'Threads', value: snapshot.cpu_threads != null ? snapshot.cpu_threads : 'N/A', tooltip: snapshot.cpu_cores ? `${snapshot.cpu_cores} cores physiques` : null },
|
||||
{ label: 'Fréquence de base', value: snapshot.cpu_base_freq_ghz ? `${snapshot.cpu_base_freq_ghz} GHz` : 'N/A', tooltip: snapshot.cpu_max_freq_ghz ? `Max: ${snapshot.cpu_max_freq_ghz} GHz` : null },
|
||||
{ label: 'Fréquence maximale', value: snapshot.cpu_max_freq_ghz ? `${snapshot.cpu_max_freq_ghz} GHz` : (cpuSockets.length > 0 && cpuSockets[0].maxSpeed !== 'N/A' ? cpuSockets[0].maxSpeed : 'N/A') },
|
||||
{ label: 'Fréquence actuelle', value: cpuSockets.length > 0 && cpuSockets[0].currentSpeed !== 'N/A' ? cpuSockets[0].currentSpeed : 'N/A' },
|
||||
{ label: 'Tension', value: cpuSockets.length > 0 ? cpuSockets[0].voltage : 'N/A' },
|
||||
{ label: 'TDP', value: snapshot.cpu_tdp_w ? `${snapshot.cpu_tdp_w} W` : 'N/A', tooltip: 'Thermal Design Power - Consommation thermique typique' },
|
||||
{ label: 'Cache L1', value: snapshot.cpu_cache_l1_kb ? utils.formatCache(snapshot.cpu_cache_l1_kb) : 'N/A', tooltip: 'Cache de niveau 1 - Le plus rapide' },
|
||||
{ label: 'Cache L2', value: snapshot.cpu_cache_l2_kb ? utils.formatCache(snapshot.cpu_cache_l2_kb) : 'N/A', tooltip: 'Cache de niveau 2 - Intermédiaire' },
|
||||
{ label: 'Cache L3', value: snapshot.cpu_cache_l3_kb ? utils.formatCache(snapshot.cpu_cache_l3_kb) : 'N/A', tooltip: 'Cache de niveau 3 - Partagé entre les cores' }
|
||||
];
|
||||
|
||||
let html = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
|
||||
${items.map(item => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);" ${item.tooltip ? `title="${utils.escapeHtml(item.tooltip)}"` : ''}>${utils.escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Multi-CPU grid
|
||||
if (cpuSockets.length > 1) {
|
||||
html += `
|
||||
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--bg-secondary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">Configuration multi-CPU (${cpuSockets.length} sockets)</div>
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.85rem;">
|
||||
<thead>
|
||||
<tr style="background: var(--bg-primary); border-bottom: 2px solid var(--border-color);">
|
||||
<th style="padding: 0.5rem; text-align: left;">Socket</th>
|
||||
<th style="padding: 0.5rem; text-align: left;">Modèle</th>
|
||||
<th style="padding: 0.5rem; text-align: center;">Cores</th>
|
||||
<th style="padding: 0.5rem; text-align: center;">Threads</th>
|
||||
<th style="padding: 0.5rem; text-align: center;">Fréq. Max</th>
|
||||
<th style="padding: 0.5rem; text-align: center;">Fréq. Actuelle</th>
|
||||
<th style="padding: 0.5rem; text-align: center;">Tension</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${cpuSockets.map((cpu, idx) => `
|
||||
<tr style="border-bottom: 1px solid var(--border-color); ${idx % 2 === 0 ? 'background: var(--bg-primary);' : ''}">
|
||||
<td style="padding: 0.5rem;">${utils.escapeHtml(cpu.socket)}</td>
|
||||
<td style="padding: 0.5rem;">${utils.escapeHtml(cpu.model)}</td>
|
||||
<td style="padding: 0.5rem; text-align: center;">${cpu.cores}</td>
|
||||
<td style="padding: 0.5rem; text-align: center;">${cpu.threads}</td>
|
||||
<td style="padding: 0.5rem; text-align: center;">${cpu.maxSpeed}</td>
|
||||
<td style="padding: 0.5rem; text-align: center;">${cpu.currentSpeed}</td>
|
||||
<td style="padding: 0.5rem; text-align: center;">${cpu.voltage}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// CPU flags
|
||||
if (snapshot.cpu_flags) {
|
||||
let flags = snapshot.cpu_flags;
|
||||
if (typeof flags === 'string') {
|
||||
try {
|
||||
flags = JSON.parse(flags);
|
||||
} catch (error) {
|
||||
flags = flags.split(',').map(flag => flag.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(flags)) {
|
||||
flags = [];
|
||||
}
|
||||
const limitedFlags = flags.slice(0, 20); // Limit to 20
|
||||
html += `
|
||||
<div style="margin-top: 1rem;">
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem;">Extensions CPU (${flags.length} total)</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 0.35rem;">
|
||||
${limitedFlags.map(flag => `
|
||||
<span style="padding: 0.2rem 0.5rem; background: var(--bg-secondary); border-radius: 3px; font-size: 0.75rem; color: var(--text-primary); border: 1px solid var(--border-color);">
|
||||
${utils.escapeHtml(flag)}
|
||||
</span>
|
||||
`).join('')}
|
||||
${flags.length > 20 ? `<span style="padding: 0.2rem 0.5rem; color: var(--text-secondary); font-size: 0.75rem;">+${flags.length - 20} autres...</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// MEMORY SECTION
|
||||
// =======================
|
||||
function renderMemoryDetails(snapshot, deviceData) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
// Parse RAM layout
|
||||
const ramLayout = snapshot.ram_layout_json ?
|
||||
(typeof snapshot.ram_layout_json === 'string' ? JSON.parse(snapshot.ram_layout_json) : snapshot.ram_layout_json) :
|
||||
[];
|
||||
|
||||
const slotsUsed = ramLayout.filter(slot => slot.size_mb > 0).length;
|
||||
const slotsTotal = snapshot.ram_slots_total || ramLayout.length || 0;
|
||||
|
||||
// ECC detection
|
||||
const hasECC = ramLayout.some(slot => slot.type_detail && slot.type_detail.toLowerCase().includes('ecc'));
|
||||
|
||||
// RAM bars data
|
||||
const ramTotal = snapshot.ram_total_mb || 0;
|
||||
const ramFree = snapshot.ram_free_mb || 0;
|
||||
const ramUsed = ramTotal - ramFree;
|
||||
const ramShared = snapshot.ram_shared_mb || 0;
|
||||
const ramUsedPercent = ramTotal > 0 ? Math.round((ramUsed / ramTotal) * 100) : 0;
|
||||
|
||||
const swapTotal = snapshot.swap_total_mb || 0;
|
||||
const swapUsed = snapshot.swap_used_mb || 0;
|
||||
const swapPercent = swapTotal > 0 ? Math.round((swapUsed / swapTotal) * 100) : 0;
|
||||
|
||||
const cards = [
|
||||
{ label: 'Capacité max carte mère', value: snapshot.ram_max_capacity_mb ? `${Math.round(snapshot.ram_max_capacity_mb / 1024)} GB` : 'N/A' },
|
||||
{ label: 'RAM Totale', value: utils.formatStorage(ramTotal, 'MB') },
|
||||
{ label: 'RAM Libre', value: utils.formatStorage(ramFree, 'MB') },
|
||||
{ label: 'RAM Utilisée', value: utils.formatStorage(ramUsed, 'MB') },
|
||||
{ label: 'RAM Partagée', value: utils.formatStorage(ramShared, 'MB') },
|
||||
{ label: 'Slots utilisés / total', value: `${slotsUsed} / ${slotsTotal}` },
|
||||
{ label: 'ECC', value: hasECC ? 'Oui' : 'Non' }
|
||||
];
|
||||
|
||||
let html = `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 0.75rem; margin-bottom: 1rem;">
|
||||
${cards.map(card => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${card.label}</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${card.value}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// RAM bar
|
||||
html += `
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; font-size: 0.85rem;">
|
||||
<span style="font-weight: 600;">RAM (${utils.formatStorage(ramTotal, 'MB')})</span>
|
||||
<span>${ramUsedPercent}% utilisée</span>
|
||||
</div>
|
||||
<div style="width: 100%; height: 24px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; border: 1px solid var(--border-color); position: relative;">
|
||||
<div style="position: absolute; top: 0; left: 0; height: 100%; width: ${ramUsedPercent}%; background: linear-gradient(to right, var(--color-warning), var(--color-danger)); transition: width 0.3s;"></div>
|
||||
</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem; display: flex; gap: 1rem;">
|
||||
<span>▮ Utilisée: ${utils.formatStorage(ramUsed, 'MB')}</span>
|
||||
<span>▯ Disponible: ${utils.formatStorage(ramFree, 'MB')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// SWAP bar
|
||||
if (swapTotal > 0) {
|
||||
html += `
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.25rem; font-size: 0.85rem;">
|
||||
<span style="font-weight: 600;">SWAP (${utils.formatStorage(swapTotal, 'MB')})</span>
|
||||
<span>${swapPercent}% utilisé</span>
|
||||
</div>
|
||||
<div style="width: 100%; height: 20px; background: var(--bg-tertiary); border-radius: 4px; overflow: hidden; border: 1px solid var(--border-color); position: relative;">
|
||||
<div style="position: absolute; top: 0; left: 0; height: 100%; width: ${swapPercent}%; background: var(--color-info); transition: width 0.3s;"></div>
|
||||
</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem;">
|
||||
▮ Utilisé: ${utils.formatStorage(swapUsed, 'MB')} | ▯ Libre: ${utils.formatStorage(swapTotal - swapUsed, 'MB')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Memory slots
|
||||
if (ramLayout && ramLayout.length > 0) {
|
||||
html += `<div class="memory-slots-grid">`;
|
||||
ramLayout.forEach((slot, idx) => {
|
||||
const slotName = slot.slot || slot.locator || `DIMM${idx}`;
|
||||
const sizeMB = slot.size_mb || 0;
|
||||
const sizeGB = sizeMB > 0 ? Math.round(sizeMB / 1024) : 0;
|
||||
const status = sizeMB > 0 ? 'occupé' : 'libre';
|
||||
const type = slot.type || 'N/A';
|
||||
const speed = slot.speed_mhz ? `${slot.speed_mhz} MT/s` : 'N/A';
|
||||
const typeDetail = slot.type_detail || 'N/A';
|
||||
const formFactor = slot.form_factor || 'N/A';
|
||||
const voltage = slot.voltage_v ? `${slot.voltage_v} V` : 'N/A';
|
||||
const manufacturer = slot.manufacturer || 'N/A';
|
||||
const serialNumber = slot.serial_number || 'N/A';
|
||||
const partNumber = slot.part_number || 'N/A';
|
||||
|
||||
html += `
|
||||
<div class="memory-slot ${sizeMB > 0 ? 'occupied' : 'empty'}" data-slot-index="${idx}">
|
||||
<div class="memory-slot-header">
|
||||
<span class="memory-slot-name">${utils.escapeHtml(slotName)}</span>
|
||||
<span class="memory-slot-size">${sizeGB > 0 ? sizeGB + 'GB' : ''}</span>
|
||||
<span class="memory-slot-status">${status}</span>
|
||||
</div>
|
||||
${sizeMB > 0 ? `
|
||||
<div class="memory-slot-details">
|
||||
<div class="memory-slot-main">${utils.escapeHtml(type)} ${utils.escapeHtml(speed)} | ${utils.escapeHtml(typeDetail)}</div>
|
||||
<div class="memory-slot-sub">${utils.escapeHtml(formFactor)} | ${utils.escapeHtml(voltage)} | ${utils.escapeHtml(manufacturer)}</div>
|
||||
<div class="memory-slot-tiny">SN: ${utils.escapeHtml(serialNumber)}</div>
|
||||
<div class="memory-slot-tiny">PN: ${utils.escapeHtml(partNumber)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// STORAGE SECTION
|
||||
// =======================
|
||||
function renderStorageDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
const storageDevices = snapshot.storage_devices_json ?
|
||||
(typeof snapshot.storage_devices_json === 'string' ? JSON.parse(snapshot.storage_devices_json) : snapshot.storage_devices_json) :
|
||||
[];
|
||||
|
||||
if (!storageDevices || storageDevices.length === 0) {
|
||||
return noData('Aucun périphérique de stockage détecté');
|
||||
}
|
||||
|
||||
return storageDevices.map(device => {
|
||||
const name = device.name || 'N/A';
|
||||
const model = device.model || 'N/A';
|
||||
const size = device.size_gb ? `${device.size_gb} GB` : 'N/A';
|
||||
const type = device.type || 'N/A';
|
||||
const smart = device.smart_status || 'N/A';
|
||||
const temp = device.temperature_c != null ? `${device.temperature_c}°C` : 'N/A';
|
||||
|
||||
const smartColor = smart.toLowerCase().includes('passed') || smart.toLowerCase().includes('ok') ? 'var(--color-success)' :
|
||||
smart.toLowerCase().includes('fail') ? 'var(--color-danger)' :
|
||||
'var(--text-secondary)';
|
||||
|
||||
return `
|
||||
<div style="padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color); margin-bottom: 0.75rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
|
||||
<div style="font-size: 2rem;">${type.toLowerCase().includes('ssd') ? '💾' : '🗄️'}</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; font-size: 1rem; color: var(--text-primary);">${utils.escapeHtml(name)}</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary);">${utils.escapeHtml(model)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 0.5rem;">
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">Capacité</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${size}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">Type</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(type)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">SMART</div>
|
||||
<div style="font-weight: 600; color: ${smartColor};">${utils.escapeHtml(smart)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">Température</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${temp}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// =======================
|
||||
// GPU SECTION
|
||||
// =======================
|
||||
function renderGPUDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
const items = [
|
||||
{ label: 'Fabricant', value: snapshot.gpu_vendor || 'N/A' },
|
||||
{ label: 'Modèle', value: snapshot.gpu_model || 'N/A' },
|
||||
{ label: 'VRAM', value: snapshot.gpu_vram_mb ? `${snapshot.gpu_vram_mb} MB` : 'N/A' },
|
||||
{ label: 'Driver', value: snapshot.gpu_driver || 'N/A' }
|
||||
];
|
||||
|
||||
return `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
|
||||
${items.map(item => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// NETWORK SECTION
|
||||
// =======================
|
||||
function renderNetworkDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
const networkInterfaces = snapshot.network_interfaces_json ?
|
||||
(typeof snapshot.network_interfaces_json === 'string' ? JSON.parse(snapshot.network_interfaces_json) : snapshot.network_interfaces_json) :
|
||||
[];
|
||||
|
||||
if (!networkInterfaces || networkInterfaces.length === 0) {
|
||||
return noData('Aucune interface réseau détectée');
|
||||
}
|
||||
|
||||
return networkInterfaces.map(iface => {
|
||||
const name = iface.name || 'N/A';
|
||||
const ipv4 = iface.ipv4 || 'N/A';
|
||||
const ipv6 = iface.ipv6 || 'N/A';
|
||||
const mac = iface.mac || 'N/A';
|
||||
const speed = iface.speed_mbps ? `${iface.speed_mbps} Mbps` : 'N/A';
|
||||
const status = iface.status || 'N/A';
|
||||
|
||||
const statusColor = status.toLowerCase().includes('up') ? 'var(--color-success)' :
|
||||
status.toLowerCase().includes('down') ? 'var(--color-danger)' :
|
||||
'var(--text-secondary)';
|
||||
|
||||
return `
|
||||
<div style="padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color); margin-bottom: 0.75rem;">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
|
||||
<div style="font-size: 2rem;">🌐</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 600; font-size: 1rem; color: var(--text-primary);">${utils.escapeHtml(name)}</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary);">${utils.escapeHtml(mac)}</div>
|
||||
</div>
|
||||
<div style="font-weight: 600; color: ${statusColor};">${utils.escapeHtml(status)}</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 0.5rem;">
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">IPv4</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary); font-family: monospace; font-size: 0.85rem;">${utils.escapeHtml(ipv4)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">IPv6</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary); font-family: monospace; font-size: 0.75rem; word-break: break-all;">${utils.escapeHtml(ipv6)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-secondary);">Vitesse</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${speed}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// =======================
|
||||
// OS SECTION
|
||||
// =======================
|
||||
function renderOSDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
const items = [
|
||||
{ label: 'OS', value: snapshot.os_name || 'N/A' },
|
||||
{ label: 'Version', value: snapshot.os_version || 'N/A' },
|
||||
{ label: 'Kernel', value: snapshot.kernel_version || 'N/A' },
|
||||
{ label: 'Architecture', value: snapshot.architecture || 'N/A' },
|
||||
{ label: 'Hostname', value: snapshot.hostname || 'N/A' },
|
||||
{ label: 'Uptime', value: snapshot.uptime_seconds ? utils.formatUptime(snapshot.uptime_seconds) : 'N/A' },
|
||||
{ label: 'Batterie', value: snapshot.battery_percent != null ? `${snapshot.battery_percent}%` : 'N/A' }
|
||||
];
|
||||
|
||||
return `
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
|
||||
${items.map(item => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// PROXMOX SECTION
|
||||
// =======================
|
||||
function renderProxmoxDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
const isProxmoxHost = snapshot.is_proxmox_host;
|
||||
const isProxmoxGuest = snapshot.is_proxmox_guest;
|
||||
const proxmoxVersion = snapshot.proxmox_version;
|
||||
|
||||
if (!isProxmoxHost && !isProxmoxGuest) {
|
||||
return noData('Non détecté comme hôte ou invité Proxmox');
|
||||
}
|
||||
|
||||
const items = [
|
||||
{ label: 'Type', value: isProxmoxHost ? 'Hôte Proxmox' : 'Invité Proxmox' },
|
||||
{ label: 'Version', value: proxmoxVersion || 'N/A' }
|
||||
];
|
||||
|
||||
return `
|
||||
<div style="padding: 1rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--color-info);">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem;">
|
||||
<div style="font-size: 2rem;">🔧</div>
|
||||
<div style="font-weight: 600; font-size: 1.1rem; color: var(--color-info);">Proxmox VE détecté</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
|
||||
${items.map(item => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-secondary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${item.label}</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(item.value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// AUDIO SECTION
|
||||
// =======================
|
||||
function renderAudioDetails(snapshot) {
|
||||
if (!snapshot) return noData();
|
||||
|
||||
const audioHardware = snapshot.audio_hardware_json ?
|
||||
(typeof snapshot.audio_hardware_json === 'string' ? JSON.parse(snapshot.audio_hardware_json) : snapshot.audio_hardware_json) :
|
||||
null;
|
||||
|
||||
const audioSoftware = snapshot.audio_software_json ?
|
||||
(typeof snapshot.audio_software_json === 'string' ? JSON.parse(snapshot.audio_software_json) : snapshot.audio_software_json) :
|
||||
null;
|
||||
|
||||
if (!audioHardware && !audioSoftware) {
|
||||
return noData('Aucune information audio disponible');
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
// Hardware section
|
||||
if (audioHardware && Array.isArray(audioHardware) && audioHardware.length > 0) {
|
||||
html += `
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">🔊 Matériel Audio</div>
|
||||
${audioHardware.map(device => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color); margin-bottom: 0.5rem;">
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(device.name || 'N/A')}</div>
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary);">${utils.escapeHtml(device.driver || 'N/A')}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Software section
|
||||
if (audioSoftware) {
|
||||
html += `
|
||||
<div>
|
||||
<div style="font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);">🎵 Logiciels Audio</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75rem;">
|
||||
${Object.entries(audioSoftware).map(([key, value]) => `
|
||||
<div style="padding: 0.75rem; background: var(--bg-primary); border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.25rem;">${utils.escapeHtml(key)}</div>
|
||||
<div style="font-weight: 600; color: var(--text-primary);">${utils.escapeHtml(String(value))}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// EXPORT PUBLIC API
|
||||
// =======================
|
||||
window.HardwareRenderer = {
|
||||
renderMotherboardDetails,
|
||||
renderCPUDetails,
|
||||
renderMemoryDetails,
|
||||
renderStorageDetails,
|
||||
renderGPUDetails,
|
||||
renderNetworkDetails,
|
||||
renderOSDetails,
|
||||
renderProxmoxDetails,
|
||||
renderAudioDetails
|
||||
};
|
||||
|
||||
})();
|
||||
251
frontend/js/icon-manager.js
Normal file
251
frontend/js/icon-manager.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Icon Manager - Gestion des packs d'icônes
|
||||
* Permet de basculer entre emojis, FontAwesome, et icônes personnalisées
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const ICON_PACKS = {
|
||||
'emoji': {
|
||||
name: 'Emojis Unicode',
|
||||
description: 'Emojis colorés par défaut',
|
||||
icons: {
|
||||
'add': '➕',
|
||||
'edit': '✏️',
|
||||
'delete': '🗑️',
|
||||
'save': '💾',
|
||||
'upload': '📤',
|
||||
'download': '📥',
|
||||
'image': '🖼️',
|
||||
'file': '📄',
|
||||
'pdf': '📕',
|
||||
'link': '🔗',
|
||||
'refresh': '🔄',
|
||||
'search': '🌍',
|
||||
'settings': '⚙️',
|
||||
'close': '❌',
|
||||
'check': '✅',
|
||||
'warning': '⚠️',
|
||||
'info': 'ℹ️',
|
||||
'copy': '📋'
|
||||
}
|
||||
},
|
||||
'fontawesome-solid': {
|
||||
name: 'FontAwesome Solid',
|
||||
description: 'Icônes FontAwesome pleines (bold)',
|
||||
icons: {
|
||||
'add': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/plus.svg" aria-hidden="true"></span>',
|
||||
'edit': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/pen-to-square.svg" aria-hidden="true"></span>',
|
||||
'delete': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/trash-can.svg" aria-hidden="true"></span>',
|
||||
'save': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/floppy-disk.svg" aria-hidden="true"></span>',
|
||||
'upload': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/upload.svg" aria-hidden="true"></span>',
|
||||
'download': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/download.svg" aria-hidden="true"></span>',
|
||||
'image': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/image.svg" aria-hidden="true"></span>',
|
||||
'file': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/file.svg" aria-hidden="true"></span>',
|
||||
'pdf': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/file-pdf.svg" aria-hidden="true"></span>',
|
||||
'link': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/link.svg" aria-hidden="true"></span>',
|
||||
'refresh': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/arrows-rotate.svg" aria-hidden="true"></span>',
|
||||
'search': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/earth-europe.svg" aria-hidden="true"></span>',
|
||||
'settings': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/gear.svg" aria-hidden="true"></span>',
|
||||
'close': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/xmark.svg" aria-hidden="true"></span>',
|
||||
'check': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/check.svg" aria-hidden="true"></span>',
|
||||
'warning': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/triangle-exclamation.svg" aria-hidden="true"></span>',
|
||||
'info': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/circle-info.svg" aria-hidden="true"></span>',
|
||||
'copy': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/copy.svg" aria-hidden="true"></span>'
|
||||
}
|
||||
},
|
||||
'fontawesome-regular': {
|
||||
name: 'FontAwesome Regular',
|
||||
description: 'Icônes FontAwesome fines (outline)',
|
||||
icons: {
|
||||
'add': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/square-plus.svg" aria-hidden="true"></span>',
|
||||
'edit': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/pen-to-square.svg" aria-hidden="true"></span>',
|
||||
'delete': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/trash-can.svg" aria-hidden="true"></span>',
|
||||
'save': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/floppy-disk.svg" aria-hidden="true"></span>',
|
||||
'upload': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/upload.svg" aria-hidden="true"></span>',
|
||||
'download': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/download.svg" aria-hidden="true"></span>',
|
||||
'image': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/image.svg" aria-hidden="true"></span>',
|
||||
'file': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/file.svg" aria-hidden="true"></span>',
|
||||
'pdf': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/file-pdf.svg" aria-hidden="true"></span>',
|
||||
'link': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/link.svg" aria-hidden="true"></span>',
|
||||
'refresh': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/arrows-rotate.svg" aria-hidden="true"></span>',
|
||||
'search': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/earth-europe.svg" aria-hidden="true"></span>',
|
||||
'settings': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/gear.svg" aria-hidden="true"></span>',
|
||||
'close': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/circle-xmark.svg" aria-hidden="true"></span>',
|
||||
'check': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/circle-check.svg" aria-hidden="true"></span>',
|
||||
'warning': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/triangle-exclamation.svg" aria-hidden="true"></span>',
|
||||
'info': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/solid/circle-info.svg" aria-hidden="true"></span>',
|
||||
'copy': '<span class="btn-icon svg-inline" data-svg-src="icons/svg/fa/regular/copy.svg" aria-hidden="true"></span>'
|
||||
}
|
||||
},
|
||||
'icons8': {
|
||||
name: 'Icons8 PNG',
|
||||
description: 'Icônes Icons8 existantes (PNG)',
|
||||
icons: {
|
||||
'add': '<img src="icons/icons8-done-48.png" class="btn-icon" alt="Add">',
|
||||
'edit': '<img src="icons/icons8-edit-pencil-48.png" class="btn-icon" alt="Edit">',
|
||||
'delete': '<img src="icons/icons8-delete-48.png" class="btn-icon" alt="Delete">',
|
||||
'save': '<img src="icons/icons8-save-48.png" class="btn-icon" alt="Save">',
|
||||
'upload': '📤',
|
||||
'download': '📥',
|
||||
'image': '<img src="icons/icons8-picture-48.png" class="btn-icon" alt="Image">',
|
||||
'file': '📄',
|
||||
'pdf': '📕',
|
||||
'link': '🔗',
|
||||
'refresh': '🔄',
|
||||
'search': '🌍',
|
||||
'settings': '<img src="icons/icons8-setting-48.png" class="btn-icon" alt="Settings">',
|
||||
'close': '<img src="icons/icons8-close-48.png" class="btn-icon" alt="Close">',
|
||||
'check': '<img src="icons/icons8-check-mark-48.png" class="btn-icon" alt="Check">',
|
||||
'warning': '⚠️',
|
||||
'info': 'ℹ️',
|
||||
'copy': '📋'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_PACK = 'emoji';
|
||||
const STORAGE_KEY = 'benchtools_icon_pack';
|
||||
const svgCache = new Map();
|
||||
|
||||
function normalizeSvg(svgText) {
|
||||
let text = svgText;
|
||||
text = text.replace(/fill="(?!none)[^"]*"/g, 'fill="currentColor"');
|
||||
text = text.replace(/stroke="(?!none)[^"]*"/g, 'stroke="currentColor"');
|
||||
return text;
|
||||
}
|
||||
|
||||
function inlineSvgElement(el) {
|
||||
const src = el.getAttribute('data-svg-src');
|
||||
if (!src) return;
|
||||
|
||||
const cached = svgCache.get(src);
|
||||
if (cached) {
|
||||
el.innerHTML = cached;
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(src)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`SVG load failed: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(text => {
|
||||
const normalized = normalizeSvg(text);
|
||||
svgCache.set(src, normalized);
|
||||
el.innerHTML = normalized;
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('[IconManager] Failed to inline SVG:', src, error);
|
||||
});
|
||||
}
|
||||
|
||||
function inlineSvgIcons(root = document) {
|
||||
const elements = root.querySelectorAll('[data-svg-src]');
|
||||
elements.forEach(el => inlineSvgElement(el));
|
||||
}
|
||||
|
||||
// Icon Manager Object
|
||||
const IconManager = {
|
||||
packs: ICON_PACKS,
|
||||
|
||||
getCurrentPack: function() {
|
||||
return localStorage.getItem(STORAGE_KEY) || DEFAULT_PACK;
|
||||
},
|
||||
|
||||
applyPack: function(packName) {
|
||||
if (!ICON_PACKS[packName]) {
|
||||
console.error(`Icon pack "${packName}" not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
localStorage.setItem(STORAGE_KEY, packName);
|
||||
|
||||
// Dispatch custom event for icon pack change
|
||||
window.dispatchEvent(new CustomEvent('iconPackChanged', {
|
||||
detail: {
|
||||
pack: packName,
|
||||
packName: ICON_PACKS[packName].name
|
||||
}
|
||||
}));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
getIcon: function(iconName, fallback = '?') {
|
||||
const currentPack = this.getCurrentPack();
|
||||
const pack = ICON_PACKS[currentPack];
|
||||
|
||||
if (!pack) {
|
||||
console.warn(`Icon pack "${currentPack}" not found`);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return pack.icons[iconName] || fallback;
|
||||
},
|
||||
|
||||
getAllPacks: function() {
|
||||
return Object.keys(ICON_PACKS);
|
||||
},
|
||||
|
||||
getPackInfo: function(packName) {
|
||||
return ICON_PACKS[packName] || null;
|
||||
},
|
||||
|
||||
// Helper pour générer un bouton avec icône
|
||||
createButton: function(iconName, text = '', className = 'btn btn-primary') {
|
||||
const icon = this.getIcon(iconName);
|
||||
const textPart = text ? ` ${text}` : '';
|
||||
return `<button class="${className}">${icon}${textPart}</button>`;
|
||||
},
|
||||
|
||||
// Helper pour mettre à jour tous les boutons de la page
|
||||
updateAllButtons: function() {
|
||||
// Cette fonction sera appelée après changement de pack
|
||||
// Pour mettre à jour dynamiquement tous les boutons
|
||||
const buttons = document.querySelectorAll('[data-icon]');
|
||||
buttons.forEach(btn => {
|
||||
const iconName = btn.getAttribute('data-icon');
|
||||
const iconSpan = btn.querySelector('.btn-icon-wrapper');
|
||||
if (iconSpan && iconName) {
|
||||
iconSpan.innerHTML = this.getIcon(iconName);
|
||||
}
|
||||
});
|
||||
inlineSvgIcons();
|
||||
},
|
||||
|
||||
inlineSvgIcons: function(root = document) {
|
||||
inlineSvgIcons(root);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-initialize on load
|
||||
function initializeIcons() {
|
||||
IconManager.updateAllButtons();
|
||||
|
||||
// Also call utils.js initializeButtonIcons if available
|
||||
if (window.initializeButtonIcons) {
|
||||
window.initializeButtonIcons();
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeIcons);
|
||||
} else {
|
||||
initializeIcons();
|
||||
}
|
||||
|
||||
// Re-initialize when icon pack changes
|
||||
window.addEventListener('iconPackChanged', function() {
|
||||
setTimeout(initializeIcons, 100); // Small delay to ensure DOM is ready
|
||||
});
|
||||
|
||||
// Expose globally
|
||||
window.IconManager = IconManager;
|
||||
|
||||
// Log initialization
|
||||
console.log(`[IconManager] Initialized with pack: ${IconManager.getCurrentPack()}`);
|
||||
})();
|
||||
@@ -26,6 +26,8 @@ async function loadBackendConfig() {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
loadDisplayPreferences();
|
||||
loadSettings();
|
||||
loadTheme();
|
||||
loadIconPack();
|
||||
await loadBackendConfig();
|
||||
});
|
||||
|
||||
@@ -37,6 +39,7 @@ function loadDisplayPreferences() {
|
||||
const temperatureUnit = localStorage.getItem('displayPref_temperatureUnit') || 'C';
|
||||
const sectionIconSize = localStorage.getItem('displayPref_sectionIconSize') || '32';
|
||||
const buttonIconSize = localStorage.getItem('displayPref_buttonIconSize') || '24';
|
||||
const searchEngine = localStorage.getItem('searchEngine') || 'google';
|
||||
|
||||
document.getElementById('memoryUnit').value = memoryUnit;
|
||||
document.getElementById('storageUnit').value = storageUnit;
|
||||
@@ -44,6 +47,7 @@ function loadDisplayPreferences() {
|
||||
document.getElementById('temperatureUnit').value = temperatureUnit;
|
||||
document.getElementById('sectionIconSize').value = sectionIconSize;
|
||||
document.getElementById('buttonIconSize').value = buttonIconSize;
|
||||
document.getElementById('searchEngine').value = searchEngine;
|
||||
|
||||
// Apply icon sizes
|
||||
applyIconSizes(sectionIconSize, buttonIconSize);
|
||||
@@ -63,6 +67,7 @@ function saveDisplayPreferences() {
|
||||
const temperatureUnit = document.getElementById('temperatureUnit').value;
|
||||
const sectionIconSize = document.getElementById('sectionIconSize').value;
|
||||
const buttonIconSize = document.getElementById('buttonIconSize').value;
|
||||
const searchEngine = document.getElementById('searchEngine').value;
|
||||
|
||||
localStorage.setItem('displayPref_memoryUnit', memoryUnit);
|
||||
localStorage.setItem('displayPref_storageUnit', storageUnit);
|
||||
@@ -70,6 +75,7 @@ function saveDisplayPreferences() {
|
||||
localStorage.setItem('displayPref_temperatureUnit', temperatureUnit);
|
||||
localStorage.setItem('displayPref_sectionIconSize', sectionIconSize);
|
||||
localStorage.setItem('displayPref_buttonIconSize', buttonIconSize);
|
||||
localStorage.setItem('searchEngine', searchEngine);
|
||||
|
||||
// Apply icon sizes immediately
|
||||
applyIconSizes(sectionIconSize, buttonIconSize);
|
||||
@@ -226,6 +232,80 @@ async function copyToken() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// THEME MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
function loadTheme() {
|
||||
const currentTheme = window.ThemeManager ? window.ThemeManager.getCurrentTheme() : 'monokai-dark';
|
||||
const select = document.getElementById('themeSelect');
|
||||
if (select) {
|
||||
select.value = currentTheme;
|
||||
}
|
||||
}
|
||||
|
||||
function saveTheme() {
|
||||
const select = document.getElementById('themeSelect');
|
||||
if (!select) return;
|
||||
|
||||
const theme = select.value;
|
||||
|
||||
if (window.ThemeManager) {
|
||||
window.ThemeManager.applyTheme(theme);
|
||||
showToast(`Thème "${theme}" appliqué avec succès`, 'success');
|
||||
} else {
|
||||
console.error('ThemeManager not available');
|
||||
showToast('Erreur: ThemeManager non disponible', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ICON PACK MANAGEMENT
|
||||
// ==========================================
|
||||
|
||||
function loadIconPack() {
|
||||
const currentPack = window.IconManager ? window.IconManager.getCurrentPack() : 'fontawesome-regular';
|
||||
const select = document.getElementById('iconPackSelect');
|
||||
if (select) {
|
||||
select.value = currentPack;
|
||||
}
|
||||
|
||||
// Initialize icon preview
|
||||
if (window.IconManager) {
|
||||
const preview = document.getElementById('iconPreview');
|
||||
if (preview) {
|
||||
window.IconManager.inlineSvgIcons(preview);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveIconPack() {
|
||||
const select = document.getElementById('iconPackSelect');
|
||||
if (!select) return;
|
||||
|
||||
const pack = select.value;
|
||||
|
||||
if (window.IconManager) {
|
||||
const success = window.IconManager.applyPack(pack);
|
||||
if (success) {
|
||||
showToast(`Pack d'icônes "${pack}" appliqué avec succès`, 'success');
|
||||
|
||||
// Refresh preview
|
||||
const preview = document.getElementById('iconPreview');
|
||||
if (preview) {
|
||||
setTimeout(() => {
|
||||
window.IconManager.inlineSvgIcons(preview);
|
||||
}, 100);
|
||||
}
|
||||
} else {
|
||||
showToast('Erreur lors de l\'application du pack d\'icônes', 'error');
|
||||
}
|
||||
} else {
|
||||
console.error('IconManager not available');
|
||||
showToast('Erreur: IconManager non disponible', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Make functions available globally
|
||||
window.generateBenchCommand = generateBenchCommand;
|
||||
window.copyGeneratedCommand = copyGeneratedCommand;
|
||||
@@ -233,3 +313,5 @@ window.toggleTokenVisibility = toggleTokenVisibility;
|
||||
window.copyToken = copyToken;
|
||||
window.saveDisplayPreferences = saveDisplayPreferences;
|
||||
window.resetDisplayPreferences = resetDisplayPreferences;
|
||||
window.saveTheme = saveTheme;
|
||||
window.saveIconPack = saveIconPack;
|
||||
|
||||
125
frontend/js/theme-manager.js
Normal file
125
frontend/js/theme-manager.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Linux BenchTools - Theme Manager
|
||||
* Handles dynamic theme loading and switching
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const THEME_STORAGE_KEY = 'benchtools_theme';
|
||||
const DEFAULT_THEME = 'monokai-dark';
|
||||
|
||||
const THEMES = {
|
||||
'monokai-dark': {
|
||||
name: 'Monokai Dark',
|
||||
file: 'css/themes/monokai-dark.css'
|
||||
},
|
||||
'monokai-light': {
|
||||
name: 'Monokai Light',
|
||||
file: 'css/themes/monokai-light.css'
|
||||
},
|
||||
'gruvbox-dark': {
|
||||
name: 'Gruvbox Dark',
|
||||
file: 'css/themes/gruvbox-dark.css'
|
||||
},
|
||||
'gruvbox-light': {
|
||||
name: 'Gruvbox Light',
|
||||
file: 'css/themes/gruvbox-light.css'
|
||||
},
|
||||
'mix-monokai-gruvbox': {
|
||||
name: 'Mix Monokai-Gruvbox',
|
||||
file: 'css/themes/mix-monokai-gruvbox.css'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current theme from localStorage
|
||||
* @returns {string} Theme identifier
|
||||
*/
|
||||
function getCurrentTheme() {
|
||||
return localStorage.getItem(THEME_STORAGE_KEY) || DEFAULT_THEME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current theme in localStorage
|
||||
* @param {string} theme - Theme identifier
|
||||
*/
|
||||
function setCurrentTheme(theme) {
|
||||
if (!THEMES[theme]) {
|
||||
console.warn(`Theme "${theme}" not found, using default`);
|
||||
theme = DEFAULT_THEME;
|
||||
}
|
||||
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a theme CSS file
|
||||
* @param {string} theme - Theme identifier
|
||||
*/
|
||||
function loadTheme(theme) {
|
||||
if (!THEMES[theme]) {
|
||||
console.warn(`Theme "${theme}" not found, using default`);
|
||||
theme = DEFAULT_THEME;
|
||||
}
|
||||
|
||||
// Remove existing theme link if present
|
||||
const existingThemeLink = document.getElementById('theme-stylesheet');
|
||||
if (existingThemeLink) {
|
||||
existingThemeLink.remove();
|
||||
}
|
||||
|
||||
// Create new theme link
|
||||
const themeLink = document.createElement('link');
|
||||
themeLink.id = 'theme-stylesheet';
|
||||
themeLink.rel = 'stylesheet';
|
||||
themeLink.href = THEMES[theme].file;
|
||||
|
||||
// Insert after the last stylesheet or in the head
|
||||
const lastStylesheet = Array.from(document.head.querySelectorAll('link[rel="stylesheet"]')).pop();
|
||||
if (lastStylesheet) {
|
||||
lastStylesheet.after(themeLink);
|
||||
} else {
|
||||
document.head.appendChild(themeLink);
|
||||
}
|
||||
|
||||
// Update body data attribute for theme-specific styling
|
||||
document.body.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme and save preference
|
||||
* @param {string} theme - Theme identifier
|
||||
*/
|
||||
function applyTheme(theme) {
|
||||
setCurrentTheme(theme);
|
||||
loadTheme(theme);
|
||||
|
||||
// Dispatch custom event for theme change
|
||||
const event = new CustomEvent('themeChanged', {
|
||||
detail: { theme, themeName: THEMES[theme].name }
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme on page load
|
||||
*/
|
||||
function initTheme() {
|
||||
const currentTheme = getCurrentTheme();
|
||||
loadTheme(currentTheme);
|
||||
}
|
||||
|
||||
// Initialize theme immediately
|
||||
initTheme();
|
||||
|
||||
// Export API to window
|
||||
window.ThemeManager = {
|
||||
getCurrentTheme,
|
||||
setCurrentTheme,
|
||||
loadTheme,
|
||||
applyTheme,
|
||||
themes: THEMES,
|
||||
defaultTheme: DEFAULT_THEME
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -96,11 +96,83 @@
|
||||
<small style="color: var(--text-muted);">Taille des icônes dans les boutons d'action</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Moteur de recherche</label>
|
||||
<select id="searchEngine" class="form-control">
|
||||
<option value="google" selected>Google</option>
|
||||
<option value="duckduckgo">DuckDuckGo</option>
|
||||
<option value="bing">Bing</option>
|
||||
</select>
|
||||
<small style="color: var(--text-muted);">Moteur utilisé pour la recherche Web du modèle</small>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="saveDisplayPreferences()">💾 Enregistrer les préférences</button>
|
||||
<button class="btn btn-secondary" onclick="resetDisplayPreferences()" style="margin-left: 0.5rem;">🔄 Réinitialiser</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Selection -->
|
||||
<div class="card">
|
||||
<div class="card-header">🎨 Thème</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Thème de l'interface</label>
|
||||
<select id="themeSelect" class="form-control">
|
||||
<option value="monokai-dark">Monokai Dark</option>
|
||||
<option value="monokai-light">Monokai Light</option>
|
||||
<option value="gruvbox-dark">Gruvbox Dark</option>
|
||||
<option value="gruvbox-light">Gruvbox Light</option>
|
||||
<option value="mix-monokai-gruvbox">Mix Monokai-Gruvbox</option>
|
||||
</select>
|
||||
<small style="color: var(--text-muted);">Changez l'apparence de l'interface</small>
|
||||
</div>
|
||||
|
||||
<div id="themePreview" style="margin-top: 1rem; padding: 1rem; border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem;">Aperçu</div>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<span style="display: inline-block; width: 40px; height: 40px; border-radius: 4px; background: var(--color-primary);" title="Primary"></span>
|
||||
<span style="display: inline-block; width: 40px; height: 40px; border-radius: 4px; background: var(--color-success);" title="Success"></span>
|
||||
<span style="display: inline-block; width: 40px; height: 40px; border-radius: 4px; background: var(--color-warning);" title="Warning"></span>
|
||||
<span style="display: inline-block; width: 40px; height: 40px; border-radius: 4px; background: var(--color-danger);" title="Danger"></span>
|
||||
<span style="display: inline-block; width: 40px; height: 40px; border-radius: 4px; background: var(--color-info);" title="Info"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="saveTheme()" style="margin-top: 1rem;">💾 Enregistrer le thème</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Icon Pack Selection -->
|
||||
<div class="card">
|
||||
<div class="card-header">🎭 Pack d'icônes</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Pack d'icônes</label>
|
||||
<select id="iconPackSelect" class="form-control">
|
||||
<option value="fontawesome-solid">FontAwesome Solid (SVG)</option>
|
||||
<option value="fontawesome-regular">FontAwesome Regular (SVG)</option>
|
||||
<option value="icons8-fluency">Icons8 Fluency (PNG)</option>
|
||||
<option value="emoji">Emoji Unicode</option>
|
||||
</select>
|
||||
<small style="color: var(--text-muted);">Les packs SVG (FontAwesome) prennent la couleur du thème</small>
|
||||
</div>
|
||||
|
||||
<div id="iconPreview" style="margin-top: 1rem; padding: 1rem; border-radius: 6px; border: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 0.5rem;">Aperçu des icônes</div>
|
||||
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center;">
|
||||
<span data-icon="save" style="font-size: 1.5rem;"></span>
|
||||
<span data-icon="edit" style="font-size: 1.5rem;"></span>
|
||||
<span data-icon="delete" style="font-size: 1.5rem;"></span>
|
||||
<span data-icon="check" style="font-size: 1.5rem;"></span>
|
||||
<span data-icon="times" style="font-size: 1.5rem;"></span>
|
||||
<span data-icon="globe" style="font-size: 1.5rem;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick="saveIconPack()" style="margin-top: 1rem;">💾 Enregistrer le pack d'icônes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bench Script Configuration -->
|
||||
<div class="card">
|
||||
<div class="card-header">⚡ Configuration Benchmark Script</div>
|
||||
|
||||
149
frontend/test-icons.html
Normal file
149
frontend/test-icons.html
Normal file
@@ -0,0 +1,149 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Icon Packs - Linux BenchTools</title>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
</head>
|
||||
<body class="test-icons-page">
|
||||
<div class="container test-icons-container">
|
||||
<div class="test-icons-hero">
|
||||
<div>
|
||||
<div class="test-icons-eyebrow">Icon Lab</div>
|
||||
<h1>🧪 Test des packs d'icônes</h1>
|
||||
<p class="test-icons-subtitle">Compare rapidement chaque pack, avec un rendu aligné sur le thème.</p>
|
||||
</div>
|
||||
<div class="test-icons-badge">Preview Live</div>
|
||||
</div>
|
||||
|
||||
<div class="card test-icons-card">
|
||||
<div class="card-header">Sélection du pack</div>
|
||||
<div class="card-body">
|
||||
<div class="test-icons-pack">
|
||||
<label class="test-icons-label" for="packSelector">Pack d'icônes</label>
|
||||
<div class="test-icons-pack-row">
|
||||
<select id="packSelector" class="form-control">
|
||||
<option value="emoji">Emojis Unicode</option>
|
||||
<option value="fontawesome-solid">FontAwesome Solid</option>
|
||||
<option value="fontawesome-regular">FontAwesome Regular</option>
|
||||
<option value="icons8">Icons8 PNG</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" onclick="applyTestPack()" data-icon="check">
|
||||
<span class="btn-icon-wrapper"></span> Appliquer le pack
|
||||
</button>
|
||||
</div>
|
||||
<div class="test-icons-hint">Les icônes sont appliquées instantanément sur les boutons ci-dessous.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card test-icons-card">
|
||||
<div class="card-header">Boutons de test</div>
|
||||
<div class="card-body">
|
||||
<div class="test-icons-actions">
|
||||
<button class="btn btn-primary" data-icon="add">
|
||||
<span class="btn-icon-wrapper"></span> Ajouter
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-icon="edit">
|
||||
<span class="btn-icon-wrapper"></span> Éditer
|
||||
</button>
|
||||
<button class="btn btn-danger" data-icon="delete">
|
||||
<span class="btn-icon-wrapper"></span> Supprimer
|
||||
</button>
|
||||
<button class="btn btn-primary" data-icon="save">
|
||||
<span class="btn-icon-wrapper"></span> Enregistrer
|
||||
</button>
|
||||
<button class="btn btn-primary" data-icon="upload">
|
||||
<span class="btn-icon-wrapper"></span> Upload
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-icon="download">
|
||||
<span class="btn-icon-wrapper"></span> Download
|
||||
</button>
|
||||
<button class="btn btn-primary" data-icon="image">
|
||||
<span class="btn-icon-wrapper"></span> Image
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-icon="file">
|
||||
<span class="btn-icon-wrapper"></span> Fichier
|
||||
</button>
|
||||
<button class="btn btn-primary" data-icon="link">
|
||||
<span class="btn-icon-wrapper"></span> Lien
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-icon="refresh">
|
||||
<span class="btn-icon-wrapper"></span> Rafraîchir
|
||||
</button>
|
||||
<button class="btn btn-primary" data-icon="search">
|
||||
<span class="btn-icon-wrapper"></span> Rechercher
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-icon="settings">
|
||||
<span class="btn-icon-wrapper"></span> Paramètres
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-icon="close">
|
||||
<span class="btn-icon-wrapper"></span> Fermer
|
||||
</button>
|
||||
<button class="btn btn-primary" data-icon="check">
|
||||
<span class="btn-icon-wrapper"></span> Valider
|
||||
</button>
|
||||
<button class="btn btn-secondary" data-icon="copy">
|
||||
<span class="btn-icon-wrapper"></span> Copier
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card test-icons-card">
|
||||
<div class="card-header">Informations de debug</div>
|
||||
<div class="card-body">
|
||||
<div id="debugInfo" class="test-icons-debug"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/theme-manager.js"></script>
|
||||
<script src="js/icon-manager.js"></script>
|
||||
<script src="js/utils.js"></script>
|
||||
<script>
|
||||
function applyTestPack() {
|
||||
const pack = document.getElementById('packSelector').value;
|
||||
window.IconManager.applyPack(pack);
|
||||
updateDebugInfo();
|
||||
|
||||
// Re-initialize icons
|
||||
if (window.initializeButtonIcons) {
|
||||
window.initializeButtonIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function updateDebugInfo() {
|
||||
const currentPack = window.IconManager.getCurrentPack();
|
||||
const packInfo = window.IconManager.getPackInfo(currentPack);
|
||||
|
||||
let info = `Pack actuel: ${currentPack}\n`;
|
||||
info += `Nom: ${packInfo.name}\n`;
|
||||
info += `Description: ${packInfo.description}\n\n`;
|
||||
|
||||
info += `Icônes disponibles:\n`;
|
||||
Object.keys(packInfo.icons).forEach(iconName => {
|
||||
const icon = packInfo.icons[iconName];
|
||||
info += ` - ${iconName}: ${icon.substring(0, 50)}${icon.length > 50 ? '...' : ''}\n`;
|
||||
});
|
||||
|
||||
document.getElementById('debugInfo').textContent = info;
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const currentPack = window.IconManager.getCurrentPack();
|
||||
document.getElementById('packSelector').value = currentPack;
|
||||
updateDebugInfo();
|
||||
});
|
||||
|
||||
// Listen for pack changes
|
||||
window.addEventListener('iconPackChanged', function(event) {
|
||||
console.log('Pack changed to:', event.detail.pack);
|
||||
updateDebugInfo();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
134
frontend/test-theme.html
Normal file
134
frontend/test-theme.html
Normal file
@@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Test Theme System</title>
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" style="margin-top: 2rem;">
|
||||
<div class="card">
|
||||
<div class="card-header">🧪 Test du système de thèmes</div>
|
||||
<div class="card-body">
|
||||
<h3>Thème actuel : <span id="currentTheme" style="color: var(--color-primary);"></span></h3>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<button class="btn btn-primary" onclick="testThemes()">🧪 Tester tous les thèmes</button>
|
||||
<button class="btn btn-secondary" onclick="showThemeInfo()">ℹ️ Infos du thème</button>
|
||||
</div>
|
||||
|
||||
<div id="testResults" style="margin-top: 1rem; padding: 1rem; background: var(--bg-tertiary); border-radius: var(--radius-md);"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/theme-manager.js"></script>
|
||||
<script src="js/icon-manager.js"></script>
|
||||
<script>
|
||||
function updateCurrentTheme() {
|
||||
const theme = window.ThemeManager.getCurrentTheme();
|
||||
document.getElementById('currentTheme').textContent = window.ThemeManager.themes[theme].name;
|
||||
}
|
||||
|
||||
function testThemes() {
|
||||
const results = document.getElementById('testResults');
|
||||
results.innerHTML = '<h4>Tests en cours...</h4>';
|
||||
|
||||
let html = '<h4 style="color: var(--color-success);">✓ Tests réussis</h4><ul>';
|
||||
|
||||
// Test 1: ThemeManager exists
|
||||
html += '<li>✓ ThemeManager est disponible</li>';
|
||||
|
||||
// Test 2: All themes are loaded
|
||||
const themes = Object.keys(window.ThemeManager.themes);
|
||||
html += `<li>✓ ${themes.length} thèmes chargés: ${themes.join(', ')}</li>`;
|
||||
|
||||
// Test 3: Default theme
|
||||
const defaultTheme = window.ThemeManager.defaultTheme;
|
||||
html += `<li>✓ Thème par défaut: ${defaultTheme}</li>`;
|
||||
|
||||
// Test 4: Current theme
|
||||
const currentTheme = window.ThemeManager.getCurrentTheme();
|
||||
html += `<li>✓ Thème actuel: ${currentTheme}</li>`;
|
||||
|
||||
// Test 5: CSS variables
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const bgPrimary = styles.getPropertyValue('--bg-primary').trim();
|
||||
const colorPrimary = styles.getPropertyValue('--color-primary').trim();
|
||||
html += `<li>✓ Variables CSS chargées (--bg-primary: ${bgPrimary}, --color-primary: ${colorPrimary})</li>`;
|
||||
|
||||
html += '</ul>';
|
||||
|
||||
html += '<h4 style="margin-top: 1rem;">🎨 Test de changement de thème</h4>';
|
||||
html += '<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">';
|
||||
|
||||
themes.forEach(theme => {
|
||||
html += `<button class="btn btn-sm ${currentTheme === theme ? 'btn-primary' : 'btn-secondary'}"
|
||||
onclick="testApplyTheme('${theme}')">
|
||||
${window.ThemeManager.themes[theme].name}
|
||||
</button>`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
results.innerHTML = html;
|
||||
}
|
||||
|
||||
function testApplyTheme(theme) {
|
||||
window.ThemeManager.applyTheme(theme);
|
||||
updateCurrentTheme();
|
||||
testThemes();
|
||||
}
|
||||
|
||||
function showThemeInfo() {
|
||||
const results = document.getElementById('testResults');
|
||||
const currentTheme = window.ThemeManager.getCurrentTheme();
|
||||
const themeInfo = window.ThemeManager.themes[currentTheme];
|
||||
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
|
||||
let html = `<h4>Informations du thème: ${themeInfo.name}</h4>`;
|
||||
html += `<p><strong>Fichier:</strong> ${themeInfo.file}</p>`;
|
||||
html += '<h5 style="margin-top: 1rem;">Variables CSS:</h5>';
|
||||
html += '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.5rem;">';
|
||||
|
||||
const variables = [
|
||||
'--bg-primary',
|
||||
'--bg-secondary',
|
||||
'--bg-tertiary',
|
||||
'--text-primary',
|
||||
'--text-secondary',
|
||||
'--color-success',
|
||||
'--color-warning',
|
||||
'--color-danger',
|
||||
'--color-info',
|
||||
'--color-primary',
|
||||
'--border-color'
|
||||
];
|
||||
|
||||
variables.forEach(varName => {
|
||||
const value = styles.getPropertyValue(varName).trim();
|
||||
html += `<div style="padding: 0.5rem; background: var(--bg-primary); border-radius: 4px;">
|
||||
<strong style="font-size: 0.8rem;">${varName}:</strong><br>
|
||||
<span style="font-family: monospace; font-size: 0.9rem;">${value}</span>
|
||||
<div style="width: 100%; height: 20px; background: ${value}; margin-top: 0.25rem; border-radius: 4px; border: 1px solid var(--border-color);"></div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
results.innerHTML = html;
|
||||
}
|
||||
|
||||
// Listen for theme changes
|
||||
window.addEventListener('themeChanged', (event) => {
|
||||
console.log('Thème changé:', event.detail);
|
||||
updateCurrentTheme();
|
||||
});
|
||||
|
||||
// Initialize
|
||||
updateCurrentTheme();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
341
frontend/theme-preview.html
Normal file
341
frontend/theme-preview.html
Normal file
@@ -0,0 +1,341 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Theme Preview - Linux BenchTools</title>
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<style>
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.theme-card:hover {
|
||||
transform: scale(1.05);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-preview-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.theme-preview-box {
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.theme-info {
|
||||
padding: 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.theme-info h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.theme-info p {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.active-indicator {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--color-success);
|
||||
color: var(--bg-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Compact Header -->
|
||||
<header style="background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%); border-bottom: 2px solid var(--color-primary); padding: 0.75rem 0;">
|
||||
<div style="max-width: 100%; padding: 0 1rem; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<h1 style="margin: 0; font-size: 1.5rem; color: var(--color-primary);">🎨 Theme Preview</h1>
|
||||
<span style="color: var(--text-secondary); font-size: 0.9rem;">Aperçu des thèmes</span>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="nav" style="margin: 0;">
|
||||
<a href="index.html" class="nav-link">Dashboard</a>
|
||||
<a href="devices.html" class="nav-link">Devices</a>
|
||||
<a href="settings.html" class="nav-link">Settings</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">🎨 Sélectionnez votre thème</div>
|
||||
<div class="card-body">
|
||||
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">
|
||||
Cliquez sur un thème pour l'appliquer immédiatement. Votre choix sera sauvegardé automatiquement.
|
||||
</p>
|
||||
|
||||
<div class="theme-grid">
|
||||
<!-- Monokai Dark -->
|
||||
<div class="theme-card" onclick="applyTheme('monokai-dark')">
|
||||
<div class="theme-preview" style="background: #1e1e1e;">
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #a6e22e;"></div>
|
||||
<div class="theme-preview-box" style="background: #66d9ef;"></div>
|
||||
<div class="theme-preview-box" style="background: #fd971f;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #f92672;"></div>
|
||||
<div class="theme-preview-box" style="background: #ae81ff;"></div>
|
||||
<div class="theme-preview-box" style="background: #e6db74;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #2d2d2d;"></div>
|
||||
<div class="theme-preview-box" style="background: #3e3e3e;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-info">
|
||||
<h3>
|
||||
Monokai Dark
|
||||
<span class="active-indicator" id="badge-monokai-dark" style="display: none;">ACTIF</span>
|
||||
</h3>
|
||||
<p>Thème sombre par défaut avec palette Monokai classique</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monokai Light -->
|
||||
<div class="theme-card" onclick="applyTheme('monokai-light')">
|
||||
<div class="theme-preview" style="background: #f9f9f9;">
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #7cb82f;"></div>
|
||||
<div class="theme-preview-box" style="background: #0099cc;"></div>
|
||||
<div class="theme-preview-box" style="background: #d87b18;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #d81857;"></div>
|
||||
<div class="theme-preview-box" style="background: #8b5fd8;"></div>
|
||||
<div class="theme-preview-box" style="background: #b8a900;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #ffffff;"></div>
|
||||
<div class="theme-preview-box" style="background: #e8e8e8;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-info">
|
||||
<h3>
|
||||
Monokai Light
|
||||
<span class="active-indicator" id="badge-monokai-light" style="display: none;">ACTIF</span>
|
||||
</h3>
|
||||
<p>Variante claire du thème Monokai pour environnements lumineux</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gruvbox Dark -->
|
||||
<div class="theme-card" onclick="applyTheme('gruvbox-dark')">
|
||||
<div class="theme-preview" style="background: #282828;">
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #b8bb26;"></div>
|
||||
<div class="theme-preview-box" style="background: #83a598;"></div>
|
||||
<div class="theme-preview-box" style="background: #fe8019;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #fb4934;"></div>
|
||||
<div class="theme-preview-box" style="background: #d3869b;"></div>
|
||||
<div class="theme-preview-box" style="background: #fabd2f;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #3c3836;"></div>
|
||||
<div class="theme-preview-box" style="background: #504945;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-info">
|
||||
<h3>
|
||||
Gruvbox Dark
|
||||
<span class="active-indicator" id="badge-gruvbox-dark" style="display: none;">ACTIF</span>
|
||||
</h3>
|
||||
<p>Palette chaleureuse et rétro inspirée de Gruvbox</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gruvbox Light -->
|
||||
<div class="theme-card" onclick="applyTheme('gruvbox-light')">
|
||||
<div class="theme-preview" style="background: #fbf1c7;">
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #98971a;"></div>
|
||||
<div class="theme-preview-box" style="background: #458588;"></div>
|
||||
<div class="theme-preview-box" style="background: #d65d0e;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #cc241d;"></div>
|
||||
<div class="theme-preview-box" style="background: #b16286;"></div>
|
||||
<div class="theme-preview-box" style="background: #d79921;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #f9f5d7;"></div>
|
||||
<div class="theme-preview-box" style="background: #ebdbb2;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-info">
|
||||
<h3>
|
||||
Gruvbox Light
|
||||
<span class="active-indicator" id="badge-gruvbox-light" style="display: none;">ACTIF</span>
|
||||
</h3>
|
||||
<p>Variante claire du thème Gruvbox, chaleureuse et rétro</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mix Monokai-Gruvbox -->
|
||||
<div class="theme-card" onclick="applyTheme('mix-monokai-gruvbox')">
|
||||
<div class="theme-preview" style="background: #1e1e1e;">
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #b8bb26;"></div>
|
||||
<div class="theme-preview-box" style="background: #83a598;"></div>
|
||||
<div class="theme-preview-box" style="background: #fe8019;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #fb4934;"></div>
|
||||
<div class="theme-preview-box" style="background: #d3869b;"></div>
|
||||
<div class="theme-preview-box" style="background: #fabd2f;"></div>
|
||||
</div>
|
||||
<div class="theme-preview-row">
|
||||
<div class="theme-preview-box" style="background: #2d2d2d;"></div>
|
||||
<div class="theme-preview-box" style="background: #3e3e3e;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-info">
|
||||
<h3>
|
||||
Mix Monokai-Gruvbox
|
||||
<span class="active-indicator" id="badge-mix-monokai-gruvbox" style="display: none;">ACTIF</span>
|
||||
</h3>
|
||||
<p>Hybride : fonds Monokai sombres + couleurs chaleureuses Gruvbox</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sample Components -->
|
||||
<div class="card">
|
||||
<div class="card-header">📦 Aperçu des composants</div>
|
||||
<div class="card-body">
|
||||
<div style="display: grid; gap: 1rem;">
|
||||
<!-- Buttons -->
|
||||
<div>
|
||||
<h4 style="color: var(--text-secondary); margin-bottom: 0.5rem;">Boutons</h4>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button class="btn btn-primary">Primary</button>
|
||||
<button class="btn btn-secondary">Secondary</button>
|
||||
<button class="btn btn-danger">Danger</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Badges -->
|
||||
<div>
|
||||
<h4 style="color: var(--text-secondary); margin-bottom: 0.5rem;">Badges</h4>
|
||||
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<span class="badge badge-success">Success</span>
|
||||
<span class="badge badge-warning">Warning</span>
|
||||
<span class="badge badge-danger">Danger</span>
|
||||
<span class="badge badge-info">Info</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<div>
|
||||
<h4 style="color: var(--text-secondary); margin-bottom: 0.5rem;">Formulaire</h4>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Exemple de champ</label>
|
||||
<input type="text" class="form-control" placeholder="Entrez du texte...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Sélection</label>
|
||||
<select class="form-control">
|
||||
<option>Option 1</option>
|
||||
<option>Option 2</option>
|
||||
<option>Option 3</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer">
|
||||
<p>© 2025 Linux BenchTools - Theme Preview</p>
|
||||
</footer>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="js/theme-manager.js"></script>
|
||||
<script src="js/icon-manager.js"></script>
|
||||
<script src="js/utils.js"></script>
|
||||
<script>
|
||||
function applyTheme(themeName) {
|
||||
if (window.ThemeManager) {
|
||||
window.ThemeManager.applyTheme(themeName);
|
||||
updateActiveIndicators(themeName);
|
||||
|
||||
if (window.BenchUtils && window.BenchUtils.showToast) {
|
||||
window.BenchUtils.showToast(
|
||||
`Thème "${window.ThemeManager.themes[themeName].name}" appliqué !`,
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateActiveIndicators(activeTheme) {
|
||||
// Hide all indicators
|
||||
document.querySelectorAll('.active-indicator').forEach(el => {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
|
||||
// Show active indicator
|
||||
const activeBadge = document.getElementById(`badge-${activeTheme}`);
|
||||
if (activeBadge) {
|
||||
activeBadge.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize active indicator on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const currentTheme = window.ThemeManager.getCurrentTheme();
|
||||
updateActiveIndicators(currentTheme);
|
||||
});
|
||||
|
||||
// Listen for theme changes
|
||||
window.addEventListener('themeChanged', (event) => {
|
||||
updateActiveIndicators(event.detail.theme);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
13
frontend/version.json
Normal file
13
frontend/version.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "2.1.3",
|
||||
"build_date": "2026-01-11",
|
||||
"features": [
|
||||
"Affichage compact des slots mémoire",
|
||||
"Bouton rafraîchissement forcé",
|
||||
"Import PCI avec pré-remplissage",
|
||||
"Champ utilisation avec hosts",
|
||||
"Détection Proxmox",
|
||||
"Inventaire motherboard détaillé",
|
||||
"Section audio hardware + logiciel"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user