feat: color picker palette + filtre/tri/duplicate matières
Deploy via Portainer / deploy (push) Successful in 0s

This commit is contained in:
Jo
2026-07-06 23:47:39 +02:00
parent 6ee16797db
commit e410bfbc16
3 changed files with 170 additions and 5 deletions
+19
View File
@@ -494,6 +494,25 @@ def fmt_time(s):
return f"{s//3600}h{(s%3600)//60:02d}m"
app.jinja_env.filters['fmt_time'] = fmt_time
_FRENCH_COLORS = {
'blanc': '#ffffff', 'noir': '#111111', 'gris': '#6b7280', 'argent': '#9ca3af',
'rouge': '#ef4444', 'rose': '#ec4899', 'fuchsia': '#d946ef',
'orange': '#f97316', 'jaune': '#eab308', 'or': '#d97706',
'vert': '#22c55e', 'olive': '#65a30d', 'emeraude': '#10b981',
'bleu': '#3b82f6', 'marine': '#1e3a8a', 'ciel': '#38bdf8', 'cyan': '#06b6d4',
'violet': '#8b5cf6', 'mauve': '#a855f7', 'indigo': '#6366f1',
'marron': '#92400e', 'beige': '#d4b896', 'creme': '#fef3c7',
'turquoise': '#14b8a6', 'or rose': '#fb7185',
'transparent': 'rgba(200,200,200,0.3)', 'naturel': '#f5f0e8',
'translucide': 'rgba(200,200,200,0.3)',
}
def css_color(value):
if not value:
return value
return _FRENCH_COLORS.get(value.strip().lower(), value)
app.jinja_env.filters['css_color'] = css_color
# ─── Dashboard ────────────────────────────────────────────────────────────────
@app.route('/')
+150 -4
View File
@@ -35,11 +35,34 @@
value="{{ material.brand if material else '' }}"
placeholder="Bambu, eSUN, Polymaker…">
</div>
<div class="col-6">
<div class="col-12">
<label class="form-label fw-semibold">Couleur</label>
<input type="text" name="color" class="form-control"
value="{{ material.color if material else '' }}"
placeholder="Rouge, #FF0000…">
<!-- Valeur soumise (hex) -->
<input type="hidden" name="color" id="colorValue"
value="{{ (material.color | css_color) if material and material.color else '' }}">
<!-- Swatches preset -->
<div id="swatchGrid" class="mb-2 d-flex flex-wrap gap-1">
<!-- Générés par JS -->
</div>
<!-- Ligne : preview + picker natif + hex -->
<div class="d-flex align-items-center gap-2">
<span id="colorPreview"
style="width:32px;height:32px;border-radius:6px;border:2px solid #dee2e6;
flex-shrink:0;background:{{ (material.color | css_color) if material and material.color else '#cccccc' }}"></span>
<input type="color" id="nativePicker" class="form-control form-control-color p-0 border-0"
style="width:36px;height:32px;cursor:pointer"
value="{{ (material.color | css_color) if material and material.color else '#cccccc' }}"
title="Choisir une couleur personnalisée">
<input type="text" id="hexInput" class="form-control form-control-sm font-monospace"
style="max-width:110px" placeholder="#rrggbb"
value="{{ (material.color | css_color) if material and material.color else '' }}">
<button type="button" class="btn btn-sm btn-outline-secondary" id="clearColor"
title="Aucune couleur">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<div class="col-6">
<label class="form-label fw-semibold">Prix (€/kg) *</label>
@@ -98,3 +121,126 @@
{% endif %}
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
// ── Palette de preset filaments ──────────────────────────────────────
const PRESETS = [
// Blancs / Neutres
{ hex: '#ffffff', label: 'Blanc' },
{ hex: '#f5f0e8', label: 'Naturel' },
{ hex: '#e8e0d0', label: 'Crème' },
{ hex: '#d4b896', label: 'Beige' },
{ hex: '#9ca3af', label: 'Argent' },
{ hex: '#6b7280', label: 'Gris' },
{ hex: '#374151', label: 'Gris foncé' },
{ hex: '#111111', label: 'Noir' },
// Rouges / Roses
{ hex: '#fca5a5', label: 'Rose clair' },
{ hex: '#f87171', label: 'Saumon' },
{ hex: '#ef4444', label: 'Rouge' },
{ hex: '#dc2626', label: 'Rouge foncé' },
{ hex: '#ec4899', label: 'Rose' },
{ hex: '#db2777', label: 'Fuchsia' },
// Oranges / Jaunes
{ hex: '#fb923c', label: 'Orange clair' },
{ hex: '#f97316', label: 'Orange' },
{ hex: '#ea580c', label: 'Orange foncé' },
{ hex: '#fbbf24', label: 'Jaune' },
{ hex: '#d97706', label: 'Or' },
{ hex: '#eab308', label: 'Jaune vif' },
// Verts
{ hex: '#86efac', label: 'Vert clair' },
{ hex: '#4ade80', label: 'Vert lime' },
{ hex: '#22c55e', label: 'Vert' },
{ hex: '#16a34a', label: 'Vert foncé' },
{ hex: '#15803d', label: 'Forêt' },
{ hex: '#65a30d', label: 'Olive' },
// Bleus / Cyans
{ hex: '#7dd3fc', label: 'Bleu ciel' },
{ hex: '#38bdf8', label: 'Ciel' },
{ hex: '#06b6d4', label: 'Cyan' },
{ hex: '#3b82f6', label: 'Bleu' },
{ hex: '#2563eb', label: 'Bleu vif' },
{ hex: '#1d4ed8', label: 'Bleu foncé' },
{ hex: '#1e3a8a', label: 'Marine' },
// Violets / Mauves
{ hex: '#c4b5fd', label: 'Lavande' },
{ hex: '#a78bfa', label: 'Mauve' },
{ hex: '#8b5cf6', label: 'Violet' },
{ hex: '#7c3aed', label: 'Violet foncé' },
{ hex: '#6366f1', label: 'Indigo' },
{ hex: '#d946ef', label: 'Magenta' },
// Marrons
{ hex: '#b45309', label: 'Marron clair' },
{ hex: '#92400e', label: 'Marron' },
{ hex: '#78350f', label: 'Marron foncé' },
// Spéciaux
{ hex: 'rgba(200,200,200,0.3)', label: 'Translucide', css: 'repeating-linear-gradient(45deg,#ccc 0,#ccc 3px,#fff 3px,#fff 8px)' },
];
const colorValue = document.getElementById('colorValue');
const colorPreview = document.getElementById('colorPreview');
const nativePicker = document.getElementById('nativePicker');
const hexInput = document.getElementById('hexInput');
const swatchGrid = document.getElementById('swatchGrid');
// ── Construire les swatches ──────────────────────────────────────────
PRESETS.forEach(p => {
const btn = document.createElement('button');
btn.type = 'button';
btn.title = p.label;
btn.dataset.hex = p.hex;
btn.style.cssText = [
'width:24px', 'height:24px', 'border-radius:5px', 'border:2px solid transparent',
`background:${p.css || p.hex}`, 'cursor:pointer', 'padding:0', 'flex-shrink:0',
'transition:transform .1s,border-color .1s'
].join(';');
btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.3)'; });
btn.addEventListener('mouseleave', () => { if (btn.dataset.hex !== colorValue.value) btn.style.transform = ''; });
btn.addEventListener('click', () => setColor(p.hex));
swatchGrid.appendChild(btn);
});
// ── Fonction centrale ────────────────────────────────────────────────
function setColor(hex) {
colorValue.value = hex;
hexInput.value = hex;
colorPreview.style.background = hex;
if (/^#[0-9a-fA-F]{6}$/.test(hex)) nativePicker.value = hex;
// Highlight swatch actif
swatchGrid.querySelectorAll('button').forEach(b => {
const active = b.dataset.hex === hex;
b.style.borderColor = active ? '#ff6b35' : 'transparent';
b.style.transform = active ? 'scale(1.3)' : '';
});
}
// ── Native color picker ──────────────────────────────────────────────
nativePicker.addEventListener('input', () => setColor(nativePicker.value));
// ── Saisie hex manuelle ──────────────────────────────────────────────
hexInput.addEventListener('input', function () {
const v = this.value.trim();
if (/^#[0-9a-fA-F]{6}$/.test(v)) setColor(v);
else { colorValue.value = v; colorPreview.style.background = v; }
});
// ── Effacer ──────────────────────────────────────────────────────────
document.getElementById('clearColor').addEventListener('click', () => {
colorValue.value = '';
hexInput.value = '';
colorPreview.style.background = '#e5e7eb';
swatchGrid.querySelectorAll('button').forEach(b => {
b.style.borderColor = 'transparent';
b.style.transform = '';
});
});
// ── Init ─────────────────────────────────────────────────────────────
const init = colorValue.value;
if (init) setColor(init);
})();
</script>
{% endblock %}
+1 -1
View File
@@ -100,7 +100,7 @@
<td>
{% if m.color %}
<span class="d-inline-flex align-items-center gap-1">
<span style="width:14px;height:14px;border-radius:50%;background:{{ m.color }};border:1px solid #ccc;display:inline-block"></span>
<span style="width:14px;height:14px;border-radius:50%;background:{{ m.color | css_color }};border:1px solid #ccc;display:inline-block"></span>
{{ m.color }}
</span>
{% else %}—{% endif %}