Part-DB-server/templates/tools/value_calculator/bulk_generate.html.twig
Sebastian Almberg d73bc42362 Add a component value calculator & image generator
Adds a "Value calculator" tool (Tools menu) that decodes/encodes the value of
common components and generates a clean SVG picture you can attach to a part —
handy for assortments imported with blank thumbnails and sparse data.

Calculator tabs (bidirectional decode <-> encode), each drawing the part to the
selected package with a collapsible appearance panel and an "attach to part"
action (optionally as the master picture):
- Resistor: 4/5/6-band colour code
- Capacitor: value <-> 3-digit code <-> tolerance
- SMD resistor: value <-> 3-digit / 4-digit / EIA-96 code
- Inductor: colour-band code
- SMD inductor: value <-> uH code

Bulk "Generate component images" parts-table action:
- Classifies each selected part (resistor / capacitor / inductor / diode / LED,
  THT or SMD) with ComponentValueGuesser and detects value, voltage, tolerance,
  power, ppm, pitch, diameter, colour, SMD package and marking from the name,
  description and parameters.
- Renders a preview per part, lets you tweak appearance, and attaches the images
  in one go; can also write matching KiCad symbol/footprint/reference-prefix EDA
  fields.

Backend: ComponentValueGuesser (classification + detection + EDA suggestion),
GeneratedImageAttachmentHelper (stores the SVG through the existing attachment
pipeline, so it is sanitised on save), two POST endpoints on PartController
(generate_image, set_eda) guarded by `edit` + CSRF, a new `@tools.value_calculator`
permission, a Tools-tree entry and docs. All drawing is client-side in a Stimulus
controller; the un-sanitised live preview escapes part-derived text and validates
colours as defence-in-depth.

Tests: unit tests for ComponentValueGuesser and functional tests for the
endpoints, including permission and CSRF enforcement and the persisted side
effects.
2026-07-30 17:49:18 +02:00

484 lines
38 KiB
Twig

{% extends "main_card.html.twig" %}
{% block title %}{% trans %}tools.bulk_gen.title{% endtrans %}{% endblock %}
{% block card_title %}
<i class="fas fa-images"></i> {% trans %}tools.bulk_gen.title{% endtrans %}
{% endblock %}
{% block card_content %}
<p class="text-muted">{% trans %}tools.bulk_gen.intro{% endtrans %}</p>
<details class="mb-3">
<summary class="text-primary" style="cursor: pointer;">
<i class="fas fa-circle-question"></i> {% trans %}tools.bulk_gen.help.title{% endtrans %}
</summary>
{# Example values as blue "chips", parameter names as grey chips — theme-aware (Bootstrap 5.3 subtle utilities). #}
{% set chip = 'badge fw-normal font-monospace bg-primary-subtle text-primary-emphasis border border-primary-subtle' %}
{% set pchip = 'badge fw-normal font-monospace bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle' %}
<div class="card card-body bg-body-tertiary mt-2">
<p class="small text-muted mb-2">{% trans %}tools.bulk_gen.help.intro{% endtrans %}</p>
<div class="table-responsive">
<table class="table table-sm mb-0 align-middle">
<thead>
<tr class="small text-uppercase text-muted">
<th class="fw-semibold">{% trans %}tools.bulk_gen.help.col_field{% endtrans %}</th>
<th class="fw-semibold">{% trans %}tools.bulk_gen.help.col_param{% endtrans %}</th>
<th class="fw-semibold">{% trans %}tools.bulk_gen.help.col_text{% endtrans %}</th>
</tr>
</thead>
<tbody>
<tr>
<td class="fw-medium">{% trans %}tools.bulk_gen.help.value{% endtrans %}</td>
<td><span class="{{ pchip }}">Resistance</span> <span class="{{ pchip }}">Capacitance</span> <span class="text-muted small">Ω / F</span></td>
<td><div class="d-flex flex-wrap gap-1"><span class="{{ chip }}">10nF</span><span class="{{ chip }}">0.1µF</span><span class="{{ chip }}">4n7</span><span class="{{ chip }}">4k7</span><span class="{{ chip }}">470R</span><span class="{{ chip }}">10k</span><span class="{{ chip }}">1M</span></div></td>
</tr>
<tr>
<td class="fw-medium">{% trans %}tools.bulk_gen.help.voltage{% endtrans %}</td>
<td><span class="{{ pchip }}">Voltage</span></td>
<td><div class="d-flex flex-wrap gap-1"><span class="{{ chip }}">50V</span><span class="{{ chip }}">100V</span></div></td>
</tr>
<tr>
<td class="fw-medium">{% trans %}tools.bulk_gen.help.tolerance{% endtrans %}</td>
<td><span class="{{ pchip }}">Tolerance</span></td>
<td><div class="d-flex flex-wrap gap-1"><span class="{{ chip }}">±5%</span><span class="{{ chip }}">10%</span></div></td>
</tr>
<tr>
<td class="fw-medium">{% trans %}tools.bulk_gen.help.pitch{% endtrans %}</td>
<td><span class="{{ pchip }}">Pitch</span> <span class="{{ pchip }}">RM</span> <span class="{{ pchip }}">Lead spacing</span></td>
<td><div class="d-flex flex-wrap gap-1"><span class="{{ chip }}">pitch 5mm</span><span class="{{ chip }}">RM5</span><span class="{{ chip }}">5mm pitch</span></div></td>
</tr>
<tr>
<td class="fw-medium">{% trans %}tools.bulk_gen.help.diameter{% endtrans %}</td>
<td><span class="{{ pchip }}">Diameter</span></td>
<td><div class="d-flex flex-wrap gap-1"><span class="{{ chip }}">⌀5mm</span><span class="{{ chip }}">diameter 5mm</span></div></td>
</tr>
<tr>
<td class="fw-medium">{% trans %}tools.bulk_gen.help.smd{% endtrans %}</td>
<td class="text-muted small">{% trans %}tools.bulk_gen.help.footprint_col{% endtrans %}</td>
<td><div class="d-flex flex-wrap gap-1"><span class="{{ chip }}">0402</span><span class="{{ chip }}">0603</span><span class="{{ chip }}">0805</span><span class="{{ chip }}">1206</span><span class="{{ chip }}">1210</span></div></td>
</tr>
</tbody>
</table>
</div>
</div>
</details>
{% if with_picture > 0 and not overwrite %}
<div class="alert alert-warning d-flex flex-wrap align-items-center justify-content-between gap-2">
<span><i class="fas fa-image"></i> {% trans with {'%count%': with_picture} %}tools.bulk_gen.with_picture{% endtrans %}</span>
<a href="{{ path('tools_bulk_generate', {ids: ids_param, overwrite: 1}) }}" class="btn btn-sm btn-warning">
<i class="fas fa-arrows-rotate"></i> {% trans with {'%count%': with_picture} %}tools.bulk_gen.regenerate{% endtrans %}
</a>
</div>
{% elseif overwrite %}
<div class="alert alert-info d-flex flex-wrap align-items-center justify-content-between gap-2">
<span><i class="fas fa-triangle-exclamation"></i> {% trans %}tools.bulk_gen.overwrite_mode{% endtrans %}</span>
<a href="{{ path('tools_bulk_generate', {ids: ids_param}) }}" class="btn btn-sm btn-outline-secondary">{% trans %}tools.bulk_gen.overwrite_exit{% endtrans %}</a>
</div>
{% endif %}
{% if selected_count == 0 or candidates is empty %}
{% if selected_count == 0 %}
<div class="alert alert-info">{% trans %}tools.bulk_gen.no_selection{% endtrans %}</div>
{% elseif with_picture == 0 %}
<div class="alert alert-warning">{% trans with {'%count%': selected_count} %}tools.bulk_gen.none{% endtrans %}</div>
{% endif %}
<a href="{{ path('homepage') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans %}tools.bulk_gen.back{% endtrans %}
</a>
{% else %}
<div {{ stimulus_controller('pages/bulkGenerate') }}>
{# One hidden value-calculator, reused to render every preview below. #}
<div class="d-none" aria-hidden="true">
{% set part = null %}
{% include 'tools/value_calculator/_calculator_body.html.twig' %}
</div>
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-2">
<p class="mb-0">
<strong>{{ candidates|length }}</strong> {% trans %}tools.bulk_gen.found{% endtrans %}
{%- if skipped > 0 %} <span class="text-muted">({{ skipped }} {% trans %}tools.bulk_gen.skipped{% endtrans %})</span>{% endif %}
</p>
<a href="{{ path('homepage') }}" class="btn btn-sm btn-outline-secondary" {{ stimulus_action('pages/bulkGenerate', 'goBack') }}>
<i class="fas fa-arrow-left"></i> {% trans %}tools.bulk_gen.back{% endtrans %}
</a>
</div>
{% set first = candidates|first %}
{% set batch_color = first.color ?: (first.type == 'capacitor' ? '#e0a63a' : (first.type == 'resistor' ? '#e8d9b5' : '#20242a')) %}
<div class="card card-body bg-body-tertiary py-2 mb-3">
<div class="d-flex flex-wrap align-items-center gap-3">
<span class="text-muted small">{% trans %}tools.bulk_gen.apply_all{% endtrans %}:</span>
{% if has_caps %}
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.cap.shape{% endtrans %}</span>
<select class="form-select" style="width: 8rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchShape') }}
{{ stimulus_action('pages/bulkGenerate', 'regenerate', 'change') }}>
<option value="disc" selected>{% trans %}tools.value_calc.cap.shape.disc{% endtrans %}</option>
<option value="blob">{% trans %}tools.value_calc.cap.shape.blob{% endtrans %}</option>
</select>
</div>
{% endif %}
{% if has_caps or has_tht_resistors or has_inductors %}
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.cap.lead{% endtrans %}</span>
<select class="form-select" style="width: 7rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchLead') }}
{{ stimulus_action('pages/bulkGenerate', 'regenerate', 'change') }}>
<option value="short">{% trans %}tools.value_calc.cap.lead.short{% endtrans %}</option>
<option value="medium" selected>{% trans %}tools.value_calc.cap.lead.medium{% endtrans %}</option>
<option value="long">{% trans %}tools.value_calc.cap.lead.long{% endtrans %}</option>
</select>
</div>
{% endif %}
{% if has_caps %}
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.cap.diameter{% endtrans %}</span>
<input type="number" class="form-control" value="5" min="1" step="0.5" style="width: 4.5rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchDiameter') }}
{{ stimulus_action('pages/bulkGenerate', 'applyDiameter', 'input') }}>
<span class="input-group-text">mm</span>
</div>
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.cap.pitch{% endtrans %}</span>
<select class="form-select" style="width: 6rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchPitch') }}
{{ stimulus_action('pages/bulkGenerate', 'applyPitch', 'change') }}>
<option value="2.54">2.54</option>
<option value="5.08" selected>5.08</option>
<option value="7.5">7.5</option>
<option value="10">10</option>
<option value="15">15</option>
</select>
</div>
{% endif %}
{% if has_tht_resistors %}
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.field.power{% endtrans %}</span>
<select class="form-select" style="width: 6rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchPower') }}
{{ stimulus_action('pages/bulkGenerate', 'applyPower', 'change') }}>
<option value="0.125">1/8 W</option>
<option value="0.25" selected>1/4 W</option>
<option value="0.5">1/2 W</option>
<option value="1">1 W</option>
<option value="2">2 W</option>
</select>
</div>
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.field.ppm{% endtrans %}</span>
<select class="form-select" style="width: 7rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchPpm') }}
{{ stimulus_action('pages/bulkGenerate', 'applyPpm', 'change') }}>
<option value="" selected>—</option>
<option value="100">100 ppm</option>
<option value="50">50 ppm</option>
<option value="25">25 ppm</option>
<option value="15">15 ppm</option>
<option value="10">10 ppm</option>
<option value="5">5 ppm</option>
</select>
</div>
{% endif %}
{% if has_smd_resistors or has_smd_inductors or has_smd_capacitors %}
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.smd.package{% endtrans %}</span>
<select class="form-select" style="width: 6rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchPackage') }}
{{ stimulus_action('pages/bulkGenerate', 'applyPackage', 'change') }}>
<option value="0201">0201</option>
<option value="0402">0402</option>
<option value="0603">0603</option>
<option value="0805" selected>0805</option>
<option value="1206">1206</option>
<option value="1210">1210</option>
<option value="2010">2010</option>
<option value="2512">2512</option>
</select>
</div>
{% endif %}
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.cap.voltage{% endtrans %}</span>
<input type="number" class="form-control" placeholder="—" min="1" style="width: 4.5rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchVoltage') }}
{{ stimulus_action('pages/bulkGenerate', 'applyVoltage', 'input') }}>
<span class="input-group-text">V</span>
</div>
<div class="input-group input-group-sm" style="width: auto;">
<span class="input-group-text">{% trans %}tools.value_calc.field.tolerance{% endtrans %}</span>
<select class="form-select" style="width: 6.5rem;"
{{ stimulus_target('pages/bulkGenerate', 'batchTolerance') }}
{{ stimulus_action('pages/bulkGenerate', 'applyTolerance', 'change') }}>
<option value="">—</option>
<option value="0.05">±0.05% (W)</option>
<option value="0.1">±0.1 (B)</option>
<option value="0.25">±0.25 (C)</option>
<option value="0.5">±0.5 (D)</option>
<option value="1">±1% (F)</option>
<option value="2">±2% (G)</option>
<option value="5">±5% (J)</option>
<option value="10">±10% (K)</option>
<option value="20">±20% (M)</option>
</select>
</div>
<div class="d-flex align-items-center gap-2">
<input type="color" class="form-control form-control-color form-control-sm" value="{{ batch_color }}"
title="{% trans %}tools.value_calc.body_color{% endtrans %}"
{{ stimulus_target('pages/bulkGenerate', 'batchColor') }}
{{ stimulus_action('pages/bulkGenerate', 'applyColor', 'input') }}>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" data-color="#e8d9b5" {{ stimulus_action('pages/bulkGenerate', 'pickColor') }}>{% trans %}tools.value_calc.body.beige{% endtrans %}</button>
<button type="button" class="btn btn-outline-secondary" data-color="#9fc6e0" {{ stimulus_action('pages/bulkGenerate', 'pickColor') }}>{% trans %}tools.value_calc.body.lightblue{% endtrans %}</button>
<button type="button" class="btn btn-outline-secondary" data-color="#2f6db0" {{ stimulus_action('pages/bulkGenerate', 'pickColor') }}>{% trans %}tools.value_calc.body.blue{% endtrans %}</button>
<button type="button" class="btn btn-outline-secondary" data-color="#b34a2f" {{ stimulus_action('pages/bulkGenerate', 'pickColor') }}>{% trans %}tools.value_calc.body.red{% endtrans %}</button>
<button type="button" class="btn btn-outline-secondary" data-color="#7b4fb0" {{ stimulus_action('pages/bulkGenerate', 'pickColor') }}>{% trans %}tools.value_calc.body.purple{% endtrans %}</button>
<button type="button" class="btn btn-outline-secondary" data-color="#303030" {{ stimulus_action('pages/bulkGenerate', 'pickColor') }}>{% trans %}tools.value_calc.body.black{% endtrans %}</button>
</div>
</div>
</div>
</div>
{# Editable KiCad column suggestions: pick a common symbol/footprint or type your own. #}
<datalist id="bulkgen-symbols">
<option value="Device:R"></option>
<option value="Device:C"></option>
<option value="Device:C_Polarized"></option>
<option value="Device:L"></option>
<option value="Device:D"></option>
<option value="Device:LED"></option>
</datalist>
<datalist id="bulkgen-footprints">
<option value="Resistor_SMD:R_0402_1005Metric"></option>
<option value="Resistor_SMD:R_0603_1608Metric"></option>
<option value="Resistor_SMD:R_0805_2012Metric"></option>
<option value="Resistor_SMD:R_1206_3216Metric"></option>
<option value="Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal"></option>
<option value="Resistor_THT:R_Axial_DIN0411_L9.9mm_D3.6mm_P12.70mm_Horizontal"></option>
<option value="Capacitor_SMD:C_0402_1005Metric"></option>
<option value="Capacitor_SMD:C_0603_1608Metric"></option>
<option value="Capacitor_SMD:C_0805_2012Metric"></option>
<option value="Capacitor_SMD:C_1206_3216Metric"></option>
<option value="Capacitor_THT:C_Disc_D3.0mm_W1.6mm_P2.50mm"></option>
<option value="Capacitor_THT:C_Disc_D3.8mm_W2.6mm_P2.50mm"></option>
<option value="Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P2.50mm"></option>
<option value="Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mm"></option>
<option value="Capacitor_THT:C_Disc_D6.0mm_W2.5mm_P5.00mm"></option>
<option value="Capacitor_THT:C_Disc_D7.5mm_W2.5mm_P5.00mm"></option>
<option value="Capacitor_THT:C_Disc_D10.0mm_W2.5mm_P5.00mm"></option>
<option value="Capacitor_THT:C_Disc_D7.5mm_W5.0mm_P7.50mm"></option>
<option value="Capacitor_THT:C_Disc_D10.5mm_W5.0mm_P7.50mm"></option>
<option value="Inductor_SMD:L_0805_2012Metric"></option>
<option value="Inductor_SMD:L_1210_3225Metric"></option>
<option value="Inductor_THT:L_Axial_L5.3mm_D2.2mm_P10.16mm_Horizontal"></option>
<option value="Diode_SMD:D_0805_2012Metric"></option>
<option value="Diode_SMD:D_SOD-123"></option>
<option value="Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal"></option>
<option value="Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal"></option>
<option value="LED_SMD:LED_0805_2012Metric"></option>
<option value="LED_THT:LED_D5.0mm"></option>
<option value="LED_THT:LED_D3.0mm"></option>
</datalist>
<p class="text-muted small mb-1"><i class="fas fa-info-circle"></i> {% trans %}tools.bulk_gen.kicad_hint{% endtrans %}</p>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th style="width: 2rem;"><input class="form-check-input" type="checkbox" checked {{ stimulus_action('pages/bulkGenerate', 'toggleAll') }}></th>
<th>{% trans %}tools.bulk_gen.part{% endtrans %}</th>
<th>{% trans %}tools.bulk_gen.type{% endtrans %}</th>
<th>{% trans %}tools.bulk_gen.value{% endtrans %}</th>
<th>{% trans %}tools.bulk_gen.appearance{% endtrans %}</th>
<th>{% trans %}tools.value_calc.field.tolerance{% endtrans %}</th>
<th>{% trans %}tools.bulk_gen.kicad{% endtrans %}</th>
<th>{% trans %}tools.bulk_gen.preview{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for c in candidates %}
{% set name_keys = {
'capacitor': 'tools.value_calc.capacitor.title',
'smd_capacitor': 'tools.value_calc.capacitor.title',
'inductor': 'tools.value_calc.inductor.title',
'smd_inductor': 'tools.value_calc.inductor.title',
'resistor': 'tools.value_calc.resistor.title',
'diode': 'tools.value_calc.diode.title',
} %}
{% set name_key = name_keys[c.type]|default('tools.value_calc.smd.title') %}
<tr {{ stimulus_target('pages/bulkGenerate', 'row') }}
data-type="{{ c.type }}"
data-subtype="{{ c.subtype }}"
data-marking="{{ c.marking }}"
data-value="{{ c.value }}"
data-voltage="{{ c.voltage }}"
data-tolerance="{{ c.tolerance ? c.tolerance|replace({'±': '', '%': ''})|trim }}"
data-package="{{ c.package }}"
data-pitch="{{ c.pitch }}"
data-diameter="{{ c.diameter }}"
data-power="{{ c.power }}"
data-ppm="{{ c.ppm }}"
data-name="{{ name_key|trans }}"
data-endpoint="{{ path('part_generate_image', {id: c.part.id}) }}"
data-csrf="{{ csrf_token('generate_image' ~ c.part.id) }}"
data-kicad-symbol="{{ c.kicad_symbol }}"
data-reference-prefix="{{ c.reference_prefix }}"
data-kicad-footprint="{{ c.kicad_footprint }}"
data-eda-endpoint="{{ path('part_set_eda', {id: c.part.id}) }}"
data-eda-csrf="{{ csrf_token('set_eda' ~ c.part.id) }}"
data-overwrite="{{ c.has_picture ? '1' : '' }}">
<td><input class="form-check-input" type="checkbox" checked></td>
<td>
<a href="{{ path('part_info', {id: c.part.id}) }}" target="_blank" rel="noopener">{{ c.part.name }}</a>
{%- if c.has_picture %} <span class="badge bg-warning text-dark" title="{{ 'tools.bulk_gen.has_picture_hint'|trans }}">{% trans %}tools.bulk_gen.has_picture{% endtrans %}</span>{% endif %}
</td>
<td><span class="badge bg-secondary">{{ c.subtype ?: c.type }}</span></td>
<td>
{%- if c.type in ['capacitor', 'smd_capacitor'] -%}
{%- if c.value >= 1e-6 %}{{ (c.value / 1e-6)|round(3) }} µF
{%- elseif c.value >= 1e-9 %}{{ (c.value / 1e-9)|round(3) }} nF
{%- else %}{{ (c.value * 1e12)|round(1) }} pF{% endif -%}
{%- elseif c.type in ['inductor', 'smd_inductor'] -%}
{%- if c.value >= 1 %}{{ c.value|round(3) }} H
{%- elseif c.value >= 1e-3 %}{{ (c.value / 1e-3)|round(3) }} mH
{%- elseif c.value >= 1e-6 %}{{ (c.value / 1e-6)|round(3) }} µH
{%- else %}{{ (c.value / 1e-9)|round(1) }} nH{% endif -%}
{%- elseif c.type == 'diode' -%}
{%- if c.marking %}{{ c.marking }}
{%- elseif c.voltage %}{{ c.voltage }} V {{ (c.subtype ?: 'diode')|capitalize }}
{%- else %}{{ (c.subtype ?: 'diode')|capitalize }}{% endif -%}
{%- else -%}
{%- if c.value >= 1e6 %}{{ (c.value / 1e6)|round(3) }}
{%- elseif c.value >= 1e3 %}{{ (c.value / 1e3)|round(3) }}
{%- else %}{{ c.value|round(3) }} Ω{% endif -%}
{%- endif -%}
{% if c.package %} · {{ c.package }}{% endif %}
</td>
<td style="min-width: 155px;">
{% set rowcolor = c.color ?: (c.type == 'capacitor' ? '#e0a63a' : (c.type == 'smd_capacitor' ? '#c8a37a' : (c.type == 'resistor' ? '#e8d9b5' : (c.type == 'inductor' ? '#2f6f4c' : (c.type == 'smd_inductor' ? '#38332e' : '#20242a'))))) %}
<div class="d-flex align-items-center gap-1 mb-1">
<input type="color" class="form-control form-control-color form-control-sm" value="{{ rowcolor }}"
data-row-color title="{% trans %}tools.value_calc.body_color{% endtrans %}"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'input') }}>
{% if c.type == 'capacitor' %}
<input type="number" class="form-control form-control-sm" value="{{ c.diameter ?: 5 }}" min="1" step="0.5" style="width: 4rem;"
data-row-diameter title="{% trans %}tools.value_calc.cap.diameter{% endtrans %} (mm)"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'input') }}>
{% endif %}
</div>
{% if c.type == 'capacitor' %}
{% set rp = c.pitch ?: 5.08 %}
<select class="form-select form-select-sm mb-1" data-row-pitch title="{% trans %}tools.value_calc.cap.pitch{% endtrans %}"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'change') }}>
<option value="2.54" {{ rp == 2.54 ? 'selected' }}>2.54</option>
<option value="5.08" {{ rp not in [2.54, 7.5, 10, 15] ? 'selected' }}>5.08</option>
<option value="7.5" {{ rp == 7.5 ? 'selected' }}>7.5</option>
<option value="10" {{ rp == 10 ? 'selected' }}>10</option>
<option value="15" {{ rp == 15 ? 'selected' }}>15</option>
</select>
{% elseif c.type == 'resistor' %}
{% set rpow = c.power ?: 0.25 %}
<div class="input-group input-group-sm">
<select class="form-select" data-row-power title="{% trans %}tools.value_calc.field.power{% endtrans %}"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'change') }}>
<option value="0.125" {{ rpow == 0.125 ? 'selected' }}>1/8 W</option>
<option value="0.25" {{ rpow not in [0.125, 0.5, 1, 2] ? 'selected' }}>1/4 W</option>
<option value="0.5" {{ rpow == 0.5 ? 'selected' }}>1/2 W</option>
<option value="1" {{ rpow == 1 ? 'selected' }}>1 W</option>
<option value="2" {{ rpow == 2 ? 'selected' }}>2 W</option>
</select>
<select class="form-select" data-row-ppm title="{% trans %}tools.value_calc.field.ppm{% endtrans %}"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'change') }}>
<option value="" {{ not c.ppm ? 'selected' }}>—</option>
<option value="100" {{ c.ppm == 100 ? 'selected' }}>100</option>
<option value="50" {{ c.ppm == 50 ? 'selected' }}>50</option>
<option value="25" {{ c.ppm == 25 ? 'selected' }}>25</option>
<option value="15" {{ c.ppm == 15 ? 'selected' }}>15</option>
<option value="10" {{ c.ppm == 10 ? 'selected' }}>10</option>
<option value="5" {{ c.ppm == 5 ? 'selected' }}>5</option>
</select>
</div>
{% elseif c.type in ['smd_resistor', 'smd_inductor', 'smd_capacitor'] %}
{% set rpkg = c.package ?: (c.type == 'smd_inductor' ? '1210' : '0805') %}
<select class="form-select form-select-sm mb-1" data-row-package title="{% trans %}tools.value_calc.smd.package{% endtrans %}"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'change') }}>
{% for pkg in ['0201', '0402', '0603', '0805', '1206', '1210', '2010', '2512'] %}
<option value="{{ pkg }}" {{ rpkg == pkg ? 'selected' }}>{{ pkg }}</option>
{% endfor %}
</select>
{% endif %}
{# Rated voltage — printed on the generated image. Not shown for LEDs (no voltage marking). #}
{% if not (c.type == 'diode' and c.subtype == 'led') %}
<div class="input-group input-group-sm">
<input type="number" class="form-control" value="{{ c.voltage }}" placeholder="V" min="1"
data-row-voltage title="{% trans %}tools.value_calc.cap.voltage{% endtrans %}"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'input') }}>
<span class="input-group-text">V</span>
</div>
{% endif %}
</td>
<td>
{% if c.type in ['diode', 'smd_capacitor'] %}
<span class="text-muted small">—</span>
{% else %}
{% set rtol = c.tolerance ? c.tolerance|replace({'±': '', '%': '', ' ': '', 'pF': '', 'PF': ''})|trim : '' %}
<select class="form-select form-select-sm" data-row-tolerance style="min-width: 7.5rem;"
{{ stimulus_action('pages/bulkGenerate', 'regenerateRow', 'change') }}>
<option value="" {{ rtol == '' ? 'selected' }}>—</option>
{% if c.type == 'capacitor' %}
<option value="0.1" {{ rtol == '0.1' ? 'selected' }}>±0.1pF (B)</option>
<option value="0.25" {{ rtol == '0.25' ? 'selected' }}>±0.25pF (C)</option>
<option value="0.5" {{ rtol == '0.5' ? 'selected' }}>±0.5pF (D)</option>
<option value="1" {{ rtol == '1' ? 'selected' }}>±1% (F)</option>
<option value="2" {{ rtol == '2' ? 'selected' }}>±2% (G)</option>
<option value="5" {{ rtol == '5' ? 'selected' }}>±5% (J)</option>
<option value="10" {{ rtol == '10' ? 'selected' }}>±10% (K)</option>
<option value="20" {{ rtol == '20' ? 'selected' }}>±20% (M)</option>
{% else %}
<option value="0.05" {{ rtol == '0.05' ? 'selected' }}>±0.05% (W)</option>
<option value="0.1" {{ rtol == '0.1' ? 'selected' }}>±0.1% (B)</option>
<option value="0.25" {{ rtol == '0.25' ? 'selected' }}>±0.25% (C)</option>
<option value="0.5" {{ rtol == '0.5' ? 'selected' }}>±0.5% (D)</option>
<option value="1" {{ rtol == '1' ? 'selected' }}>±1% (F)</option>
<option value="2" {{ rtol == '2' ? 'selected' }}>±2% (G)</option>
<option value="5" {{ rtol == '5' ? 'selected' }}>±5% (J)</option>
<option value="10" {{ rtol == '10' ? 'selected' }}>±10% (K)</option>
{% endif %}
</select>
{% endif %}
</td>
<td class="small" style="min-width: 230px;">
<input type="text" class="form-control form-control-sm font-monospace mb-1" value="{{ c.kicad_symbol }}"
data-bulk-eda-symbol list="bulkgen-symbols" placeholder="Device:C">
<div class="input-group input-group-sm">
<input type="text" class="form-control font-monospace" value="{{ c.kicad_footprint }}"
data-bulk-eda-footprint list="bulkgen-footprints" placeholder="{% trans %}tools.bulk_gen.footprint_ph{% endtrans %}">
<input type="text" class="form-control text-center" style="max-width: 3.5rem;" value="{{ c.reference_prefix }}"
data-bulk-eda-reference placeholder="Ref" title="{% trans %}tools.bulk_gen.reference{% endtrans %}">
</div>
</td>
<td><div data-bulk-preview class="bg-light rounded p-1 text-center" style="width: 180px;"></div></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<button type="button" class="btn btn-success" {{ stimulus_target('pages/bulkGenerate', 'attachBtn') }} {{ stimulus_action('pages/bulkGenerate', 'attachSelected') }}>
<i class="fas fa-paperclip"></i> {% trans %}tools.bulk_gen.generate_attach{% endtrans %}
</button>
<button type="button" class="btn btn-primary" {{ stimulus_target('pages/bulkGenerate', 'edaBtn') }} {{ stimulus_action('pages/bulkGenerate', 'writeEda') }}>
<i class="fas fa-microchip"></i> {% trans %}tools.bulk_gen.write_eda{% endtrans %}
</button>
<div class="progress flex-grow-1 d-none" style="height: 1.5rem; min-width: 12rem;" {{ stimulus_target('pages/bulkGenerate', 'progress') }}>
<div class="progress-bar" role="progressbar" style="width: 0" {{ stimulus_target('pages/bulkGenerate', 'progressBar') }}>0/0</div>
</div>
<a href="{{ path('homepage') }}" class="btn btn-outline-secondary ms-auto" {{ stimulus_action('pages/bulkGenerate', 'goBack') }}>
<i class="fas fa-arrow-left"></i> {% trans %}tools.bulk_gen.back{% endtrans %}
</a>
</div>
</div>
{% endif %}
{% endblock %}