diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json
index 0f0ad0fe..c0ff32d9 100644
--- a/frontend/public/components-metadata.json
+++ b/frontend/public/components-metadata.json
@@ -1450,7 +1450,7 @@
"defaultValues": {
"i2cAddress": "0x3c"
},
- "pinCount": 8,
+ "pinCount": 0,
"tags": [
"ssd1306",
"oled",
@@ -1460,6 +1460,35 @@
"0x3c"
]
},
+ {
+ "thumbnail": "",
+ "tags": [
+ "ssd1306",
+ "oled",
+ "display",
+ "i2c",
+ "4-pin",
+ "0x3c"
+ ],
+ "properties": [
+ {
+ "name": "i2cAddress",
+ "type": "string",
+ "defaultValue": "0x3c",
+ "control": "text",
+ "description": "I2C address"
+ }
+ ],
+ "defaultValues": {
+ "i2cAddress": "0x3c"
+ },
+ "pinCount": 4,
+ "$comment": "4-pin I2C-only SSD1306 module (GND/VCC/SCL/SDA) — the cheap 0.96\" OLED most beginners have; velxio-ssd1306-i2c native element. Counterpart to the 8-pin wokwi-ssd1306. Issue #215.",
+ "id": "ssd1306-i2c-4pin",
+ "tagName": "velxio-ssd1306-i2c-4pin",
+ "name": "SSD1306 OLED (I2C, 4-pin)",
+ "category": "displays"
+ },
{
"thumbnail": "",
"tags": [
@@ -3325,6 +3354,44 @@
"led"
]
},
+ {
+ "thumbnail": "",
+ "tags": [
+ "breadboard",
+ "protoboard",
+ "prototype",
+ "passive",
+ "wokwi-breadboard"
+ ],
+ "properties": [],
+ "defaultValues": {},
+ "pinCount": 830,
+ "$comment": "Velxio-native passive: full 830-point breadboard. id matches the wokwi part type (wokwi-breadboard strips to 'breadboard') so wokwi zip import/export works with no alias. Internal 5-hole strips + power rails are joined in NetlistBuilder/traceDetailed via utils/breadboardNets.ts.",
+ "id": "breadboard",
+ "tagName": "velxio-breadboard",
+ "name": "Breadboard (full)",
+ "category": "passive",
+ "description": "830-point solderless breadboard: 63 columns x two 5-hole banks (a-e / f-j) plus four full-length power rails. Holes in the same column strip or rail are electrically connected."
+ },
+ {
+ "thumbnail": "",
+ "tags": [
+ "breadboard",
+ "mini",
+ "protoboard",
+ "passive",
+ "wokwi-breadboard-mini"
+ ],
+ "properties": [],
+ "defaultValues": {},
+ "pinCount": 170,
+ "$comment": "Velxio-native passive: 170-point mini breadboard (17x2 banks, no rails). Same wokwi-compatible naming as the full breadboard.",
+ "id": "breadboard-mini",
+ "tagName": "velxio-breadboard-mini",
+ "name": "Breadboard Mini",
+ "category": "passive",
+ "description": "170-point mini solderless breadboard: 17 columns x two 5-hole banks (a-e / f-j), no power rails. Holes in the same column strip are electrically connected."
+ },
{
"thumbnail": "",
"tags": [
@@ -4451,34 +4518,6 @@
"motion",
"sensor"
]
- },
- {
- "id": "ssd1306-i2c-4pin",
- "tagName": "velxio-ssd1306-i2c-4pin",
- "name": "SSD1306 OLED (I2C, 4-pin)",
- "category": "displays",
- "pinCount": 4,
- "tags": [
- "ssd1306",
- "oled",
- "display",
- "i2c",
- "4-pin",
- "0x3c"
- ],
- "thumbnail": "",
- "properties": [
- {
- "name": "i2cAddress",
- "type": "string",
- "defaultValue": "0x3c",
- "control": "text",
- "description": "I2C address"
- }
- ],
- "defaultValues": {
- "i2cAddress": "0x3c"
- }
}
]
}
\ No newline at end of file
diff --git a/frontend/src/__tests__/breadboard-nets.test.ts b/frontend/src/__tests__/breadboard-nets.test.ts
new file mode 100644
index 00000000..e8e34377
--- /dev/null
+++ b/frontend/src/__tests__/breadboard-nets.test.ts
@@ -0,0 +1,124 @@
+/**
+ * Breadboard internal-connectivity tests.
+ *
+ * A breadboard joins holes in fixed groups (5-hole column strips, power
+ * rails). The joining is modelled at the union-find level in NetlistBuilder
+ * (see unionBreadboardGroups) and keyed by utils/breadboardNets.ts — these
+ * tests pin the group semantics and the resulting nets end-to-end through
+ * buildNetlist / buildWireNetMap.
+ */
+import { describe, it, expect } from 'vitest';
+import { breadboardGroupKey, isBreadboard } from '../utils/breadboardNets';
+import { buildNetlist, buildWireNetMap } from '../simulation/spice/NetlistBuilder';
+
+describe('breadboardGroupKey', () => {
+ it('groups the 5 holes of a column strip', () => {
+ const keys = ['a', 'b', 'c', 'd', 'e'].map((r) => breadboardGroupKey('breadboard', `18t.${r}`));
+ expect(new Set(keys).size).toBe(1);
+ expect(keys[0]).toBe('col18t');
+ });
+
+ it('separates top and bottom banks of the same column', () => {
+ expect(breadboardGroupKey('breadboard', '18t.a')).not.toBe(
+ breadboardGroupKey('breadboard', '18b.f'),
+ );
+ });
+
+ it('separates different columns', () => {
+ expect(breadboardGroupKey('breadboard', '17t.a')).not.toBe(
+ breadboardGroupKey('breadboard', '18t.a'),
+ );
+ });
+
+ it('groups a whole power rail as one net', () => {
+ expect(breadboardGroupKey('breadboard', 'bn.1')).toBe('railbn');
+ expect(breadboardGroupKey('breadboard', 'bn.50')).toBe('railbn');
+ expect(breadboardGroupKey('breadboard', 'bp.3')).toBe('railbp');
+ expect(breadboardGroupKey('breadboard', 'tp.9')).toBe('railtp');
+ });
+
+ it('works for the mini breadboard and rejects non-breadboards', () => {
+ expect(breadboardGroupKey('breadboard-mini', '5t.c')).toBe('col5t');
+ expect(breadboardGroupKey('resistor', '1')).toBeNull();
+ expect(breadboardGroupKey('breadboard', 'not-a-hole')).toBeNull();
+ expect(isBreadboard('breadboard')).toBe(true);
+ expect(isBreadboard('breadboard-mini')).toBe(true);
+ expect(isBreadboard('pushbutton')).toBe(false);
+ });
+});
+
+describe('breadboard nets in NetlistBuilder', () => {
+ const BOARD = {
+ id: 'arduino',
+ vcc: 5,
+ pins: { '5V': { type: 'digital', v: 5 }, GND: { type: 'digital', v: 0 } },
+ groundPinNames: ['GND'],
+ vccPinNames: ['5V'],
+ };
+
+ it('buildWireNetMap: two wires into the same column strip share a net', () => {
+ const map = buildWireNetMap({
+ components: [
+ { id: 'bb1', metadataId: 'breadboard', properties: {} },
+ { id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
+ ],
+ wires: [
+ {
+ id: 'w-in',
+ start: { componentId: 'arduino', pinName: '5V' },
+ end: { componentId: 'bb1', pinName: '10t.a' },
+ },
+ {
+ id: 'w-out',
+ start: { componentId: 'bb1', pinName: '10t.e' },
+ end: { componentId: 'r1', pinName: '1' },
+ },
+ ],
+ boards: [BOARD],
+ } as never);
+ expect(map.get('w-in')).toBeDefined();
+ expect(map.get('w-in')).toBe(map.get('w-out'));
+ });
+
+ it('buildWireNetMap: different columns stay separate nets', () => {
+ const map = buildWireNetMap({
+ components: [{ id: 'bb1', metadataId: 'breadboard', properties: {} }],
+ wires: [
+ {
+ id: 'w-a',
+ start: { componentId: 'arduino', pinName: '5V' },
+ end: { componentId: 'bb1', pinName: '10t.a' },
+ },
+ {
+ id: 'w-b',
+ start: { componentId: 'bb1', pinName: '11t.a' },
+ end: { componentId: 'arduino', pinName: 'GND' },
+ },
+ ],
+ boards: [BOARD],
+ } as never);
+ expect(map.get('w-a')).not.toBe(map.get('w-b'));
+ });
+
+ it('buildNetlist: a resistor fed through breadboard strips + rail resolves to vcc/gnd nets', () => {
+ // 5V → rail tp.1 | rail tp.30 → column 5t.a | 5t.c → R1.1 | R1.2 → 20b.f | 20b.j → GND
+ const { netlist } = buildNetlist({
+ components: [
+ { id: 'bb1', metadataId: 'breadboard', properties: {} },
+ { id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
+ ],
+ wires: [
+ { id: 'w1', start: { componentId: 'arduino', pinName: '5V' }, end: { componentId: 'bb1', pinName: 'tp.1' } },
+ { id: 'w2', start: { componentId: 'bb1', pinName: 'tp.30' }, end: { componentId: 'bb1', pinName: '5t.a' } },
+ { id: 'w3', start: { componentId: 'bb1', pinName: '5t.c' }, end: { componentId: 'r1', pinName: '1' } },
+ { id: 'w4', start: { componentId: 'r1', pinName: '2' }, end: { componentId: 'bb1', pinName: '20b.f' } },
+ { id: 'w5', start: { componentId: 'bb1', pinName: '20b.j' }, end: { componentId: 'arduino', pinName: 'GND' } },
+ ],
+ boards: [BOARD],
+ analysis: { kind: 'op' },
+ } as never);
+ // The resistor's card must connect vcc_rail directly to ground — every
+ // breadboard hop collapsed into the two canonical nets.
+ expect(netlist).toMatch(/R_r1 (vcc_rail 0|0 vcc_rail) 1000/);
+ });
+});
diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx
index b6b114cd..ec2f55f6 100644
--- a/frontend/src/components/DynamicComponent.tsx
+++ b/frontend/src/components/DynamicComponent.tsx
@@ -31,6 +31,7 @@ import { syntheticChipPin } from '../simulation/customChips/syntheticPins';
import { resolveChipNetKey } from '../simulation/customChips/chipNets';
import { getMixedModeScheduler } from '../simulation/spice/MixedModeScheduler';
import { getBoardLogicFamily } from '../simulation/LogicFamilies';
+import { breadboardGroupKey } from '../utils/breadboardNets';
// Side-effect imports: register every web component we'll create at runtime.
// `@wokwi/elements` covers the upstream catalog; `../velxio-elements` adds
@@ -170,6 +171,29 @@ function traceDetailed(
);
if (result.arduinoPin !== null) return result;
}
+
+ // Breadboards join N holes per internal group (5-hole strip / power
+ // rail), which the 2-terminal PASSIVE_PIN_PAIRS map can't express.
+ // Continue the trace from every OTHER wired hole in the same group.
+ const bbGroup = comp && breadboardGroupKey(comp.metadataId, otherEp.pinName);
+ if (bbGroup && comp) {
+ const groupPins = new Set();
+ for (const gw of state.wires) {
+ for (const ep of [gw.start, gw.end]) {
+ if (
+ ep.componentId === comp.id &&
+ ep.pinName !== otherEp.pinName &&
+ breadboardGroupKey(comp.metadataId, ep.pinName) === bbGroup
+ ) {
+ groupPins.add(ep.pinName);
+ }
+ }
+ }
+ for (const groupPin of groupPins) {
+ const result = traceDetailed(state, comp.id, groupPin, depth + 1, activeSeen);
+ if (result.arduinoPin !== null) return result;
+ }
+ }
}
}
diff --git a/frontend/src/simulation/spice/NetlistBuilder.ts b/frontend/src/simulation/spice/NetlistBuilder.ts
index a966fc28..968367a6 100644
--- a/frontend/src/simulation/spice/NetlistBuilder.ts
+++ b/frontend/src/simulation/spice/NetlistBuilder.ts
@@ -16,8 +16,40 @@
*/
import { UnionFind } from './unionFind';
import { componentToSpice } from './componentToSpice';
+import { isBreadboard, breadboardGroupKey } from '../../utils/breadboardNets';
import type { BuildNetlistInput, ComponentForSpice, BoardForSpice, WireForSpice } from './types';
+/**
+ * Union the wired pins of every breadboard that share an internal group
+ * (5-hole column or power rail). Breadboard connectivity is static, so it is
+ * modelled at the union-find level — no SPICE cards needed — which makes it
+ * equally visible to buildNetlist, the circuit verifier, and the overlay
+ * net maps (all of which run their own bare union-find over wires).
+ */
+function unionBreadboardGroups(
+ uf: UnionFind,
+ components: ReadonlyArray<{ id: string; metadataId: string }>,
+ wires: WireForSpice[],
+ pinKey: (componentId: string, pinName: string) => string,
+): void {
+ for (const comp of components) {
+ if (!isBreadboard(comp.metadataId)) continue;
+ const anchors = new Map(); // groupKey → first pin key
+ for (const pinName of pinsReferencedByWires(comp.id, wires)) {
+ const group = breadboardGroupKey(comp.metadataId, pinName);
+ if (!group) continue;
+ const key = pinKey(comp.id, pinName);
+ uf.add(key);
+ const anchor = anchors.get(group);
+ if (anchor) {
+ uf.union(anchor, key);
+ } else {
+ anchors.set(group, key);
+ }
+ }
+ }
+}
+
// Matches GND, VSS, VEE, GROUND and any numbered ground pin in the common
// spellings boards actually emit: GND.1 / GND.2 (dotted), GND2 / GND3 (bare),
// GND_2 (underscore). Several dev-kit board elements label their extra ground
@@ -104,6 +136,9 @@ export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult {
}
}
+ // Breadboards: join wired holes that share an internal strip/rail.
+ unionBreadboardGroups(uf, components, wires, pinKey);
+
// ── 2. Canonicalize ground / VCC pins ────────────────────────────────────
for (const board of boards) {
for (const pinName of board.groundPinNames ?? []) {
@@ -457,6 +492,9 @@ export function buildWireNetMap(
uf.union(a, b);
}
+ // Breadboards: join wired holes that share an internal strip/rail.
+ unionBreadboardGroups(uf, components, wires, pin);
+
for (const board of boards) {
for (const pName of board.groundPinNames ?? []) uf.setCanonical(pin(board.id, pName), '0');
for (const pName of board.vccPinNames ?? []) uf.setCanonical(pin(board.id, pName), 'vcc_rail');
@@ -504,6 +542,9 @@ export function buildBoardPinNetMap(
uf.union(a, b);
}
+ // Breadboards: join wired holes that share an internal strip/rail.
+ unionBreadboardGroups(uf, components, wires, pin);
+
// Canonicalize board ground/vcc pins (from boardPinGroups metadata)
for (const board of boards) {
for (const pName of board.groundPinNames ?? []) {
diff --git a/frontend/src/utils/breadboardNets.ts b/frontend/src/utils/breadboardNets.ts
new file mode 100644
index 00000000..7f2d264a
--- /dev/null
+++ b/frontend/src/utils/breadboardNets.ts
@@ -0,0 +1,41 @@
+/**
+ * Breadboard internal-connectivity helpers.
+ *
+ * A solderless breadboard electrically joins holes in fixed groups:
+ * - each terminal-strip column of 5 holes is one net
+ * (full board: rows a-e form the top bank, f-j the bottom bank), and
+ * - each power rail (full board only) is one net running the whole length.
+ *
+ * Pin names follow the Wokwi convention so imported diagram.json wires work
+ * verbatim (e.g. "18t.d" = column 18, top bank, row d; "bn.15" = bottom
+ * negative rail, hole 15):
+ * holes: `${col}t.${a-e}` | `${col}b.${f-j}`
+ * rails: `tp.N` / `tn.N` (top +/-) | `bp.N` / `bn.N` (bottom +/-)
+ *
+ * Consumers (NetlistBuilder union-find, DynamicComponent digital trace) use
+ * `breadboardGroupKey` to decide which pins are internally shorted: two pins
+ * on the same breadboard belong to the same net iff their group keys match.
+ */
+
+const BREADBOARD_IDS = new Set(['breadboard', 'breadboard-mini']);
+
+/** True when the metadataId is one of the breadboard parts. */
+export function isBreadboard(metadataId: string): boolean {
+ return BREADBOARD_IDS.has(metadataId);
+}
+
+const HOLE_RE = /^(\d+)([tb])\.([a-j])$/;
+const RAIL_RE = /^([tb][pn])\.(\d+)$/;
+
+/**
+ * Group key for a breadboard pin, or null when the pin name is not a valid
+ * breadboard hole/rail. Pins with equal keys are internally connected.
+ */
+export function breadboardGroupKey(metadataId: string, pinName: string): string | null {
+ if (!isBreadboard(metadataId)) return null;
+ const hole = HOLE_RE.exec(pinName);
+ if (hole) return `col${hole[1]}${hole[2]}`; // e.g. "col18t" — one 5-hole strip
+ const rail = RAIL_RE.exec(pinName);
+ if (rail) return `rail${rail[1]}`; // e.g. "railbn" — the whole rail is one net
+ return null;
+}
diff --git a/frontend/src/velxio-elements/breadboard-element.ts b/frontend/src/velxio-elements/breadboard-element.ts
new file mode 100644
index 00000000..881adb02
--- /dev/null
+++ b/frontend/src/velxio-elements/breadboard-element.ts
@@ -0,0 +1,145 @@
+/**
+ * — full-size 830-point solderless breadboard.
+ *
+ * 63 terminal-strip columns × two 5-hole banks (rows a-e top, f-j bottom)
+ * plus four power rails (+/- top and bottom, 50 holes each in 5-hole groups).
+ * Pin names follow the Wokwi convention (see utils/breadboardNets.ts):
+ * holes `${col}t.${a-e}` / `${col}b.${f-j}`, rails `tp.N` `tn.N` `bp.N` `bn.N`.
+ *
+ * Purely passive: the electrical joining of each column/rail happens in
+ * NetlistBuilder + the digital trace via breadboardGroupKey — this element
+ * only renders and exposes pinInfo. The pinInfo array is precomputed at
+ * module load (830 entries) so the getter stays cheap for the pin overlay,
+ * which re-reads it on every measure (see CLAUDE.md §6a).
+ */
+
+const PITCH = 9.6; // 0.1in in wokwi-elements CSS px
+const COLS = 63;
+const RAIL_HOLES = 50; // 10 groups of 5
+
+// Layout (CSS px). Terminal grid is centered; rails hug the long edges.
+const GRID_LEFT = 19.2; // x of column 1
+const RAIL_LEFT = GRID_LEFT + ((COLS - 1) * PITCH - (RAIL_HOLES - 1 + 9) * PITCH) / 2;
+const ROW_Y: Record = {
+ // top rails
+ 'tp': 16.8,
+ 'tn': 26.4,
+ // bank a-e
+ a: 55.2, b: 64.8, c: 74.4, d: 84.0, e: 93.6,
+ // bank f-j (below the center channel)
+ f: 122.4, g: 132.0, h: 141.6, i: 151.2, j: 160.8,
+ // bottom rails
+ 'bp': 189.6,
+ 'bn': 199.2,
+};
+const WIDTH = GRID_LEFT * 2 + (COLS - 1) * PITCH; // 614.4
+const HEIGHT = 216;
+
+function railX(n: number): number {
+ // 10 groups of 5 with one extra pitch between groups
+ const idx = n - 1;
+ return RAIL_LEFT + (idx + Math.floor(idx / 5)) * PITCH;
+}
+
+export interface BreadboardPin {
+ name: string;
+ x: number;
+ y: number;
+ number: number;
+ signals: [];
+}
+
+function buildPins(): BreadboardPin[] {
+ const pins: BreadboardPin[] = [];
+ let n = 1;
+ for (const rail of ['tp', 'tn', 'bp', 'bn'] as const) {
+ for (let i = 1; i <= RAIL_HOLES; i++) {
+ pins.push({ name: `${rail}.${i}`, x: railX(i), y: ROW_Y[rail], number: n++, signals: [] });
+ }
+ }
+ for (let col = 1; col <= COLS; col++) {
+ const x = GRID_LEFT + (col - 1) * PITCH;
+ for (const row of ['a', 'b', 'c', 'd', 'e'] as const) {
+ pins.push({ name: `${col}t.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
+ }
+ for (const row of ['f', 'g', 'h', 'i', 'j'] as const) {
+ pins.push({ name: `${col}b.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
+ }
+ }
+ return pins;
+}
+
+const PINS: readonly BreadboardPin[] = buildPins();
+
+function holeRect(x: number, y: number): string {
+ return ``;
+}
+
+function buildSvg(): string {
+ const parts: string[] = [];
+ parts.push(
+ `');
+ return parts.join('');
+}
+
+let svgCache: string | null = null;
+
+export class BreadboardElement extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ }
+
+ connectedCallback() {
+ if (!svgCache) svgCache = buildSvg();
+ this.shadowRoot!.innerHTML = svgCache;
+ }
+
+ get pinInfo() {
+ return PINS as unknown as Array<{ name: string; x: number; y: number; number: number }>;
+ }
+}
+
+if (!customElements.get('velxio-breadboard')) {
+ customElements.define('velxio-breadboard', BreadboardElement);
+}
diff --git a/frontend/src/velxio-elements/breadboard-mini-element.ts b/frontend/src/velxio-elements/breadboard-mini-element.ts
new file mode 100644
index 00000000..28fa8a72
--- /dev/null
+++ b/frontend/src/velxio-elements/breadboard-mini-element.ts
@@ -0,0 +1,98 @@
+/**
+ * — 170-point mini solderless breadboard.
+ *
+ * 17 terminal-strip columns × two 5-hole banks (rows a-e top, f-j bottom),
+ * no power rails. Pin names follow the Wokwi convention:
+ * `${col}t.${a-e}` / `${col}b.${f-j}` (see utils/breadboardNets.ts).
+ *
+ * Passive — internal column joining happens in NetlistBuilder / the digital
+ * trace via breadboardGroupKey. pinInfo (170 entries) is precomputed at
+ * module load so the getter stays cheap for the pin overlay.
+ */
+
+const PITCH = 9.6;
+const COLS = 17;
+const GRID_LEFT = 14.4;
+const ROW_Y: Record = {
+ a: 14.4, b: 24.0, c: 33.6, d: 43.2, e: 52.8,
+ f: 76.8, g: 86.4, h: 96.0, i: 105.6, j: 115.2,
+};
+const WIDTH = GRID_LEFT * 2 + (COLS - 1) * PITCH; // 182.4
+const HEIGHT = 129.6;
+
+export interface BreadboardMiniPin {
+ name: string;
+ x: number;
+ y: number;
+ number: number;
+ signals: [];
+}
+
+function buildPins(): BreadboardMiniPin[] {
+ const pins: BreadboardMiniPin[] = [];
+ let n = 1;
+ for (let col = 1; col <= COLS; col++) {
+ const x = GRID_LEFT + (col - 1) * PITCH;
+ for (const row of ['a', 'b', 'c', 'd', 'e'] as const) {
+ pins.push({ name: `${col}t.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
+ }
+ for (const row of ['f', 'g', 'h', 'i', 'j'] as const) {
+ pins.push({ name: `${col}b.${row}`, x, y: ROW_Y[row], number: n++, signals: [] });
+ }
+ }
+ return pins;
+}
+
+const PINS: readonly BreadboardMiniPin[] = buildPins();
+
+function buildSvg(): string {
+ const parts: string[] = [];
+ parts.push(
+ `');
+ return parts.join('');
+}
+
+let svgCache: string | null = null;
+
+export class BreadboardMiniElement extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ }
+
+ connectedCallback() {
+ if (!svgCache) svgCache = buildSvg();
+ this.shadowRoot!.innerHTML = svgCache;
+ }
+
+ get pinInfo() {
+ return PINS as unknown as Array<{ name: string; x: number; y: number; number: number }>;
+ }
+}
+
+if (!customElements.get('velxio-breadboard-mini')) {
+ customElements.define('velxio-breadboard-mini', BreadboardMiniElement);
+}
diff --git a/frontend/src/velxio-elements/index.ts b/frontend/src/velxio-elements/index.ts
index 495921e5..714409fd 100644
--- a/frontend/src/velxio-elements/index.ts
+++ b/frontend/src/velxio-elements/index.ts
@@ -18,7 +18,11 @@ import './capacitor-element';
import './capacitor-electrolytic-element';
import './inductor-element';
import './custom-chip-element';
+import './breadboard-element';
+import './breadboard-mini-element';
export { CapacitorElement } from './capacitor-element';
export { CapacitorElectrolyticElement } from './capacitor-electrolytic-element';
export { InductorElement } from './inductor-element';
+export { BreadboardElement } from './breadboard-element';
+export { BreadboardMiniElement } from './breadboard-mini-element';
diff --git a/scripts/component-overrides.json b/scripts/component-overrides.json
index b69ea800..3c0bc741 100644
--- a/scripts/component-overrides.json
+++ b/scripts/component-overrides.json
@@ -2484,6 +2484,44 @@
"defaultValues": {
"i2cAddress": "0x3c"
}
+ },
+ {
+ "$comment": "Velxio-native passive: full 830-point breadboard. id matches the wokwi part type (wokwi-breadboard strips to 'breadboard') so wokwi zip import/export works with no alias. Internal 5-hole strips + power rails are joined in NetlistBuilder/traceDetailed via utils/breadboardNets.ts.",
+ "id": "breadboard",
+ "tagName": "velxio-breadboard",
+ "name": "Breadboard (full)",
+ "category": "passive",
+ "pinCount": 830,
+ "tags": [
+ "breadboard",
+ "protoboard",
+ "prototype",
+ "passive",
+ "wokwi-breadboard"
+ ],
+ "thumbnail": "",
+ "properties": [],
+ "defaultValues": {},
+ "description": "830-point solderless breadboard: 63 columns x two 5-hole banks (a-e / f-j) plus four full-length power rails. Holes in the same column strip or rail are electrically connected."
+ },
+ {
+ "$comment": "Velxio-native passive: 170-point mini breadboard (17x2 banks, no rails). Same wokwi-compatible naming as the full breadboard.",
+ "id": "breadboard-mini",
+ "tagName": "velxio-breadboard-mini",
+ "name": "Breadboard Mini",
+ "category": "passive",
+ "pinCount": 170,
+ "tags": [
+ "breadboard",
+ "mini",
+ "protoboard",
+ "passive",
+ "wokwi-breadboard-mini"
+ ],
+ "thumbnail": "",
+ "properties": [],
+ "defaultValues": {},
+ "description": "170-point mini solderless breadboard: 17 columns x two 5-hole banks (a-e / f-j), no power rails. Holes in the same column strip are electrically connected."
}
],
"led": {