fusion_plating: icon picker grid + auto-icon from name (v19.0.2.0.3)

Replaced text input with a clickable 24-icon grid picker for the side
panel. Icons are curated for plating (flask, blast, mask, rinse, bake,
plate, inspect, etc.). When adding a new step, the icon is automatically
guessed from the name via keyword matching (e.g. "Masking" → paint-brush,
"Oven baking" → fire, "Acid Dip" → flask).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 15:01:46 -04:00
parent edc7b11cb6
commit 3316b5d519
3 changed files with 105 additions and 8 deletions

View File

@@ -33,6 +33,63 @@ const NODE_TYPE_OPTIONS = [
{ value: "step", label: "Step" },
];
// ---- Icon picker options (curated for plating / manufacturing) -----------
const ICON_OPTIONS = [
{ value: "fa-flask", label: "Flask / Chemistry" },
{ value: "fa-industry", label: "Industry / Line" },
{ value: "fa-sitemap", label: "Sitemap / Process" },
{ value: "fa-wrench", label: "Wrench / Operation" },
{ value: "fa-cog", label: "Gear / General" },
{ value: "fa-cogs", label: "Gears / System" },
{ value: "fa-paint-brush", label: "Paint / Masking" },
{ value: "fa-eraser", label: "Eraser / De-Masking" },
{ value: "fa-th", label: "Grid / Racking" },
{ value: "fa-fire", label: "Fire / Bake" },
{ value: "fa-bolt", label: "Bolt / Electric" },
{ value: "fa-diamond", label: "Diamond / Plating" },
{ value: "fa-tint", label: "Tint / Rinse" },
{ value: "fa-shower", label: "Shower / Clean" },
{ value: "fa-bullseye", label: "Target / Blast" },
{ value: "fa-search", label: "Search / Inspect" },
{ value: "fa-check-circle", label: "Check / Approve" },
{ value: "fa-clock-o", label: "Clock / Wait" },
{ value: "fa-sun-o", label: "Sun / Dry" },
{ value: "fa-thermometer-half", label: "Temp / Heat" },
{ value: "fa-eye", label: "Eye / Visual" },
{ value: "fa-hand-paper-o", label: "Hand / Manual" },
{ value: "fa-cube", label: "Cube / Part" },
{ value: "fa-shield", label: "Shield / Protect" },
];
// ---- Auto-icon: guess the best icon from the node name ------------------
const ICON_KEYWORDS = [
{ pattern: /mask/i, icon: "fa-paint-brush" },
{ pattern: /de-?mask|unmask/i, icon: "fa-eraser" },
{ pattern: /rack/i, icon: "fa-th" },
{ pattern: /de-?rack|unrack/i, icon: "fa-th" },
{ pattern: /blast/i, icon: "fa-bullseye" },
{ pattern: /bake|oven/i, icon: "fa-fire" },
{ pattern: /clean|soak|wash/i, icon: "fa-shower" },
{ pattern: /rinse/i, icon: "fa-tint" },
{ pattern: /dry/i, icon: "fa-sun-o" },
{ pattern: /nickel|plate|plat/i, icon: "fa-diamond" },
{ pattern: /strike|electro/i, icon: "fa-bolt" },
{ pattern: /acid|dip|etch/i, icon: "fa-flask" },
{ pattern: /inspect|check|test/i, icon: "fa-search" },
{ pattern: /ready|wait|queue/i, icon: "fa-clock-o" },
{ pattern: /line|process/i, icon: "fa-industry" },
{ pattern: /heat|temp/i, icon: "fa-thermometer-half" },
{ pattern: /porosity/i, icon: "fa-tint" },
];
function guessIcon(name) {
if (!name) return "fa-cog";
for (const rule of ICON_KEYWORDS) {
if (rule.pattern.test(name)) return rule.icon;
}
return "fa-cog";
}
export class RecipeTreeEditor extends Component {
static template = "fusion_plating.RecipeTreeEditor";
static props = ["*"];
@@ -205,6 +262,7 @@ export class RecipeTreeEditor extends Component {
parent_id: this.state.addingTo,
name: name,
node_type: this.state.newNodeType,
vals: { icon: guessIcon(name) },
});
if (result && result.ok) {
this.notification.add(`Added "${name}"`, { type: "success" });
@@ -391,6 +449,10 @@ export class RecipeTreeEditor extends Component {
return NODE_TYPE_OPTIONS;
}
getIconOptions() {
return ICON_OPTIONS;
}
formatDuration(minutes) {
if (!minutes) return "";
if (minutes < 60) return `${Math.round(minutes)}m`;

View File

@@ -372,6 +372,40 @@
}
}
// ---- Icon picker ------------------------------------------------------------
.o_fp_recipe_icon_picker {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.o_fp_recipe_icon_btn {
width: 34px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid $border-color;
border-radius: 6px;
background: transparent;
color: var(--bs-secondary-color);
font-size: 0.9rem;
cursor: pointer;
transition: border-color 0.12s, background-color 0.12s;
&:hover {
border-color: var(--o-action, var(--bs-primary));
color: var(--bs-body-color);
}
&.active {
background: var(--o-action, var(--bs-primary));
border-color: var(--o-action, var(--bs-primary));
color: #fff;
}
}
// ---- Responsive -------------------------------------------------------------
@media (max-width: 768px) {

View File

@@ -100,14 +100,15 @@
</select>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Icon (FA class)</label>
<div class="input-group">
<span class="input-group-text">
<i t-att-class="'fa ' + (state.selectedNode.icon || 'fa-cog')"/>
</span>
<input type="text" class="form-control"
t-att-value="state.selectedNode.icon"
t-on-change="(ev) => { state.selectedNode.icon = ev.target.value; }"/>
<label class="form-label fw-bold">Icon</label>
<div class="o_fp_recipe_icon_picker">
<t t-foreach="getIconOptions()" t-as="ic" t-key="ic.value">
<button t-att-class="'o_fp_recipe_icon_btn' + (state.selectedNode.icon === ic.value ? ' active' : '')"
t-on-click.stop="() => { state.selectedNode.icon = ic.value; }"
t-att-title="ic.label">
<i t-att-class="'fa ' + ic.value"/>
</button>
</t>
</div>
</div>
<div class="mb-3">