app
Component Widgets
Public Made by Adomby adom
Reusable component widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and lightweight four-level LOD cache listings.
← Commit history
Publish 1.0.0
19 files changed
+959−58
README.md+32−51SKILL.md+101assets/example-molecule.pngassets/example-pinout.pngexamples/heatsink-mcp23017.json+1examples/mol-stm32f103.json+1examples/pinout-stm32f103.json+1install.sh+21package.json+58page.json+54−7uninstall.sh+3widgets/chip-thumbnail-widget.js+38widgets/heatsink-via-widget.js+152widgets/heatsink-viewer.html+33widgets/molecule-viewer.html+21widgets/molecule-widget.js+113widgets/pinout-viewer.html+55widgets/pinout-widget.js+256widgets/thumbnail-viewer.html+19README.md+32−51@@ -1,69 +1,50 @@ # Component Widgets -> **This page renders `readme.html`, not this file.** `readme.html` shadows `README.md`-> on the wiki page (the Overview tab serves the HTML in a sandboxed, script-running-> iframe), so the four widgets below are **live and interactive on the page**, not-> images. This README stays for the Files tab, `repo clone`, and git tooling.+Reusable, dependency-free web-component widgets for component and molecule pages.+Install drops the widget JS, embeddable viewers, example data, and an AI skill into+`~/.claude/skills/component-widgets/`. -One home for the reusable widgets you drop into any component or molecule page. Each is a-self-contained web component — no build, no dependencies.+ -**How they work:** feed each widget via `src="…json"`, an inline-`<script type="application/json">`, or `el.data = {…}`. Shadow-DOM isolated, light + dark.-They all speak the **adom-lbr cross-highlight bus** (`BroadcastChannel('lbr-hover')`): the-pin-keyed widgets (pinout, footprint, 3D, heat-sink via) light up the same pin together on-hover.+ -## Pinout — `<adom-pinout>`-Datasheet-style chip diagram: pins on the sides, colored by type, exposed pad centered. The-diagram and the table cross-highlight each other. Give it real pad `xy` and it lays out from-the true footprint; omit it and it infers a quad/dual package.--```html-<adom-pinout><script type="application/json">-{ "mpn": "…", "package": "…", "pins": [ { "number": 1, "name": "…", "type": "…", "xy": [x,y] } ] }-</script></adom-pinout>+```+adom-wiki pkg install adom/component-widgets ``` -## Heat-sink via — `<adom-heatsink-via>` · new-The recommended thermal-via array under the exposed pad (via count, drill, pitch). **Only-renders when the part actually has an exposed pad**, otherwise an empty state. Keyed on the-EP's pin number so it lights when you hover that pad in the pinout/footprint.+## The widgets -```html-<adom-heatsink-via><script type="application/json">-{ "mpn": "MCP23017", "package": "QFN-28-1EP…EP4.25x4.25mm", "ep": { "w_mm": 4.25, "h_mm": 4.25, "number": 29 } }-</script></adom-heatsink-via>-```+| Tag | What it renders | Data |+|---|---|---|+| `<adom-pinout>` | Datasheet-style pin diagram + synced pin table (cross-highlight) | `{ mpn, package, pins:[{number,name,type?,xy?}] }` |+| `<adom-heatsink-via>` | Exposed-pad thermal via array (only when the part has an EP) | `{ mpn, package, ep:{w_mm,h_mm,number} }` or `{ pads:[…] }` |+| `<adom-molecule>` | Ganged part set + nets (hover a net → its parts) | `{ name, components:[…], nets:[…] }` |+| `<adom-chip-thumbnail>` | Rendered part image with caption | `src` / `mpn` / `package` | -## Ganged molecule — `<adom-molecule>` · new-A molecule as its ganged part set + how they connect. Hover a net → its parts; hover a part → its nets.+Each is one custom element: Shadow DOM, no dependencies, light + dark (or+`theme="dark|light"`), fed by inline `<script type="application/json">`, `el.data`,+or `src=`. All speak one `BroadcastChannel('lbr-hover')` bus, so pin-keyed widgets+(pinout, heat-sink via, and adom-lbr's footprint + 3D) light the same pin together. -```html-<adom-molecule><script type="application/json">-{ "name": "…", "components": [ {"ref":"…","mpn":"…","value":"…","package":"…","type":"…"} ],- "nets": [ {"name":"…","pins":[ {"ref":"…","pin":"…"} ]} ] }-</script></adom-molecule>-```+## Use in a page -## Chip thumbnail — `<adom-chip-thumbnail>` · new-A small rendered image of the actual part (`glb2png` / the chip-thumbnailer off its GLB).+Copy a `widgets/*-widget.js` into your HTML and add the tag: ```html-<adom-chip-thumbnail src="thumb.png" mpn="74HC00" package="SOIC-14"></adom-chip-thumbnail>+<script src="pinout-widget.js"></script>+<adom-pinout><script type="application/json">+{ "mpn": "STM32F103C8T6", "package": "LQFP-48", "pins": [ … ] }+</script></adom-pinout> ``` -## Symbol · Footprint · 3D — **adom-lbr**-Provided by **adom-lbr**'s viewers (canonical adom-lbr JSON, footprint layer stackup KiCad +-Altium, first-class pin table, per-lead 3D highlight). Embed the adom-lbr slot/viewer — don't-reimplement. They already share the `lbr-hover` bus, so they cross-link with the widgets above.+In a wiki `readme.html` the sandbox has no network, so feed **inline JSON** (never+`src=`) and inline the widget JS. See the `wiki-readme-html` skill and the live+reference page `adom/component-widgets`, whose own `readme.html` runs all four. -## Datasheet — native viewer-Push the part's PDF; the native datasheet viewer renders it inline with pop-out. No widget to build.+## Not here — use adom-lbr +Schematic **symbol**, **footprint** (layer stackup), and **3D** are adom-lbr's+viewers, already on the same hover bus. The **datasheet** renders via the native+PDF viewer. Don't reimplement those. -**Attached:** `pinout-widget.js` · `heatsink-via-widget.js` · `chip-thumbnail-widget.js` ·-`molecule-widget.js` — plus each widget's `*-viewer.html` (iframe-embeddable, `?src=`/`?part=`/-postMessage, `readme-height` auto-grow) and the example JSON sources. `readme.html` is the-built, self-contained page (widget JS + fonts + data all inlined) that the wiki renders.+See `SKILL.md` for the full data contracts and the bus protocol.
SKILL.mdadded+101@@ -0,0 +1,101 @@+---+name: component-widgets+description: >-+ Reusable, dependency-free web-component widgets to drop into any component or+ molecule page: <adom-pinout> (datasheet-style pin diagram + table),+ <adom-heatsink-via> (exposed-pad thermal via array), <adom-molecule> (ganged+ part set + nets), <adom-chip-thumbnail> (rendered part image). Self-contained+ (Shadow DOM, no deps, light + dark), fed by inline JSON or el.data, and all on+ one lbr-hover cross-highlight bus. Use when building a wiki component page's+ readme.html, or any HTML page, that needs a live pinout, a thermal-via pattern,+ a molecule/sub-circuit view, or a part thumbnail. Symbol/footprint/3D are NOT+ here (use adom-lbr). Trigger words: pinout widget, adom-pinout, pin diagram,+ heat-sink via, thermal via array, exposed pad vias, molecule widget, ganged+ components, chip thumbnail, component page widgets, readme-drop-in widget.+user-invocable: true+---++# Component Widgets++Four reusable web components for component/molecule pages. Each is one custom+element, self-contained (Shadow DOM, no dependencies), theme-aware (light + dark,+or pin `theme="dark"|"light"`), and fed three ways:++- inline: `<adom-x><script type="application/json">{ ... }</script></adom-x>`+- property: `document.querySelector('adom-x').data = { ... }`+- fetch: `<adom-x src="data.json"></adom-x>` (only where network is allowed)++Installed to `~/.claude/skills/component-widgets/widgets/`. Copy a `*-widget.js`+into your page and add the tag. Live reference page: `adom/component-widgets`.++## In a wiki readme.html, inline JSON only (no fetch)++A wiki `readme.html` renders in a sandboxed, opaque-origin iframe with **no+network** (`connect-src 'none'`). So feed widgets **inline JSON**, never `src=`.+Inline the `*-widget.js` verbatim in a `<script>` block, base64 any images, add+the `readme-height` reporter, and shim `localStorage`/`BroadcastChannel` (they can+throw in the sandbox). See the `wiki-readme-html` skill; the reference page's own+`readme.html` is a worked example.++## The widgets++### `<adom-pinout>` — pin diagram + table+Datasheet-style chip: pins on the sides colored by type, exposed pad centered, with+a synced pin table. Diagram and table cross-highlight each other.+```+{ "mpn": "STM32F103C8T6", "package": "LQFP-48",+ "pins": [ { "number": 1, "name": "VBAT", "type": "power", "xy": [-2.75, 4.25] } ] }+```+`type` is `power|io|clock|ctrl|other` (auto-inferred from `name` if omitted). `xy`+is the mm pad center (footprint frame, +y up); with real `xy` it lays out from the+true footprint, otherwise it infers a quad/dual package from `package`. Attributes:+`layout="auto|quad|dual"`, `heading`. Public method `highlight(n)`; emits `pinhover`.++### `<adom-heatsink-via>` — exposed-pad thermal vias · new+The recommended thermal-via array under the exposed pad (count, drill, pitch).+**Only renders when the part actually has an exposed pad**, otherwise an empty state.+```+{ "mpn": "MCP23017", "package": "QFN-28-1EP_6x6mm_P0.65mm_EP4.25x4.25mm",+ "ep": { "w_mm": 4.25, "h_mm": 4.25, "number": 29 } }+```+Or give it `pads: [{number,x_mm,y_mm}]` and it detects the EP geometrically. Keyed on+the EP pin number, so it lights when that pad is hovered in the pinout or footprint.++### `<adom-molecule>` — ganged parts + nets · new+A molecule (reusable sub-circuit) as its part set plus how they connect. Hover a net+to light its parts; hover a part to light its nets.+```+{ "name": "STM32F103 support",+ "components": [ { "ref": "U1", "mpn": "STM32F103C8T6", "package": "LQFP-48", "type": "mcu" } ],+ "nets": [ { "name": "VDD", "pins": [ { "ref": "U1", "pin": "VDD_1" }, { "ref": "C1", "pin": "1" } ] } ] }+```+`type` is `mcu|ic|cap|res|xtal|diode|conn|other` (inferred from the refdes if omitted).++### `<adom-chip-thumbnail>` — rendered part image · new+```+<adom-chip-thumbnail src="thumb.png" mpn="74HC00" package="SOIC-14"></adom-chip-thumbnail>+```+`src` is a rendered PNG (in production: `adom-glb2png` / the chip-thumbnailer off the+part's GLB). Falls back to a clean placeholder when there is no render.++## The cross-highlight bus++Every widget speaks one channel: `BroadcastChannel('lbr-hover')` (with a+`parent.postMessage` relay for cross-iframe). Pin-keyed widgets — pinout,+heat-sink via, and adom-lbr's footprint + 3D — post and receive+`{ type:'lbr-hover', pin:<number> }`, so hovering a pin in one lights the same pin+everywhere. The molecule posts `{ ...ref }` (its own ganged relationship) rather+than a pin. Any widgets sharing a document (or relayed across iframes) gang up+automatically; no wiring needed.++## What is NOT here — use adom-lbr++Schematic **symbol**, **footprint** (layer stackup), and **3D** already exist as+adom-lbr's viewers, on the same `lbr-hover` bus. Embed those; do not reimplement+them. The **datasheet** renders via the wiki's native PDF viewer (just push the PDF).++## Viewers++Each widget has an iframe-embeddable `*-viewer.html` in `widgets/` (`?src=`/`?part=`/+postMessage, `?theme=`, `readme-height` auto-grow) for embedding where live iframes+are supported.
assets/example-molecule.pngadded⋯ 1 unchanged line ⋯
assets/example-pinout.pngadded⋯ 1 unchanged line ⋯
examples/heatsink-mcp23017.jsonadded+1@@ -0,0 +1 @@+{"mpn":"MCP23017","package":"QFN-28-1EP_6x6mm_P0.65mm_EP4.25x4.25mm","ep":{"w_mm":4.25,"h_mm":4.25,"number":29}}
examples/mol-stm32f103.jsonadded+1@@ -0,0 +1 @@+{"name": "STM32F103 \u00b7 minimum support", "components": [{"ref": "U1", "mpn": "STM32F103C8T6", "value": "STM32F103C8", "package": "LQFP-48", "type": "mcu"}, {"ref": "C1", "value": "100nF", "package": "0402", "type": "cap"}, {"ref": "C2", "value": "100nF", "package": "0402", "type": "cap"}, {"ref": "C3", "value": "100nF", "package": "0402", "type": "cap"}, {"ref": "C4", "value": "4.7uF", "package": "0805", "type": "cap"}, {"ref": "C5", "value": "10nF", "package": "0402", "type": "cap"}, {"ref": "C6", "value": "1uF", "package": "0603", "type": "cap"}, {"ref": "Y1", "value": "8MHz", "package": "HC-49", "type": "xtal"}, {"ref": "C7", "value": "20pF", "package": "0402", "type": "cap"}, {"ref": "C8", "value": "20pF", "package": "0402", "type": "cap"}, {"ref": "R1", "value": "10k", "package": "0402", "type": "res"}, {"ref": "C9", "value": "100nF", "package": "0402", "type": "cap"}], "nets": [{"name": "VDD", "pins": [{"ref": "U1", "pin": "VDD_1"}, {"ref": "U1", "pin": "VDD_2"}, {"ref": "U1", "pin": "VDD_3"}, {"ref": "C1", "pin": "1"}, {"ref": "C2", "pin": "1"}, {"ref": "C3", "pin": "1"}, {"ref": "C4", "pin": "1"}]}, {"name": "VDDA", "pins": [{"ref": "U1", "pin": "VDDA"}, {"ref": "C5", "pin": "1"}, {"ref": "C6", "pin": "1"}]}, {"name": "OSC_IN", "pins": [{"ref": "U1", "pin": "PD0"}, {"ref": "Y1", "pin": "1"}, {"ref": "C7", "pin": "1"}]}, {"name": "OSC_OUT", "pins": [{"ref": "U1", "pin": "PD1"}, {"ref": "Y1", "pin": "2"}, {"ref": "C8", "pin": "1"}]}, {"name": "NRST", "pins": [{"ref": "U1", "pin": "NRST"}, {"ref": "C9", "pin": "1"}]}, {"name": "BOOT0", "pins": [{"ref": "U1", "pin": "BOOT0"}, {"ref": "R1", "pin": "1"}]}, {"name": "GND", "pins": [{"ref": "U1", "pin": "VSS_1"}, {"ref": "U1", "pin": "VSSA"}, {"ref": "C1", "pin": "2"}, {"ref": "C4", "pin": "2"}, {"ref": "C9", "pin": "2"}, {"ref": "R1", "pin": "2"}]}]}
examples/pinout-stm32f103.jsonadded+1@@ -0,0 +1 @@+{"mpn": "STM32F103C8T6", "package": "LQFP-48", "pins": [{"number": 1, "name": "VBAT", "xy": [-2.75, 4.25], "type": "power"}, {"number": 2, "name": "PC13-TAMPER-RTC", "xy": [-2.25, 4.25], "type": "io"}, {"number": 3, "name": "PC14-OSC32_IN", "xy": [-1.75, 4.25], "type": "clock"}, {"number": 4, "name": "PC15-OSC32_OUT", "xy": [-1.25, 4.25], "type": "clock"}, {"number": 5, "name": "PD0-OSC_IN", "xy": [-0.75, 4.25], "type": "clock"}, {"number": 6, "name": "PD1-OSC_OUT", "xy": [-0.25, 4.25], "type": "clock"}, {"number": 7, "name": "NRST", "xy": [0.25, 4.25], "type": "ctrl"}, {"number": 8, "name": "VSSA", "xy": [0.75, 4.25], "type": "power"}, {"number": 9, "name": "VDDA", "xy": [1.25, 4.25], "type": "power"}, {"number": 10, "name": "PA0-WKUP", "xy": [1.75, 4.25], "type": "io"}, {"number": 11, "name": "PA1", "xy": [2.25, 4.25], "type": "io"}, {"number": 12, "name": "PA2", "xy": [2.75, 4.25], "type": "io"}, {"number": 13, "name": "PA3", "xy": [4.25, 2.75], "type": "io"}, {"number": 14, "name": "PA4", "xy": [4.25, 2.25], "type": "io"}, {"number": 15, "name": "PA5", "xy": [4.25, 1.75], "type": "io"}, {"number": 16, "name": "PA6", "xy": [4.25, 1.25], "type": "io"}, {"number": 17, "name": "PA7", "xy": [4.25, 0.75], "type": "io"}, {"number": 18, "name": "PB0", "xy": [4.25, 0.25], "type": "io"}, {"number": 19, "name": "PB1", "xy": [4.25, -0.25], "type": "io"}, {"number": 20, "name": "PB2", "xy": [4.25, -0.75], "type": "io"}, {"number": 21, "name": "PB10", "xy": [4.25, -1.25], "type": "io"}, {"number": 22, "name": "PB11", "xy": [4.25, -1.75], "type": "io"}, {"number": 23, "name": "VSS_1", "xy": [4.25, -2.25], "type": "power"}, {"number": 24, "name": "VDD_1", "xy": [4.25, -2.75], "type": "power"}, {"number": 25, "name": "PB12", "xy": [2.75, -4.25], "type": "io"}, {"number": 26, "name": "PB13", "xy": [2.25, -4.25], "type": "io"}, {"number": 27, "name": "PB14", "xy": [1.75, -4.25], "type": "io"}, {"number": 28, "name": "PB15", "xy": [1.25, -4.25], "type": "io"}, {"number": 29, "name": "PA8", "xy": [0.75, -4.25], "type": "io"}, {"number": 30, "name": "PA9", "xy": [0.25, -4.25], "type": "io"}, {"number": 31, "name": "PA10", "xy": [-0.25, -4.25], "type": "io"}, {"number": 32, "name": "PA11", "xy": [-0.75, -4.25], "type": "io"}, {"number": 33, "name": "PA12", "xy": [-1.25, -4.25], "type": "io"}, {"number": 34, "name": "PA13", "xy": [-1.75, -4.25], "type": "io"}, {"number": 35, "name": "VSS_2", "xy": [-2.25, -4.25], "type": "power"}, {"number": 36, "name": "VDD_2", "xy": [-2.75, -4.25], "type": "power"}, {"number": 37, "name": "PA14", "xy": [-4.25, -2.75], "type": "io"}, {"number": 38, "name": "PA15", "xy": [-4.25, -2.25], "type": "io"}, {"number": 39, "name": "PB3", "xy": [-4.25, -1.75], "type": "io"}, {"number": 40, "name": "PB4", "xy": [-4.25, -1.25], "type": "io"}, {"number": 41, "name": "PB5", "xy": [-4.25, -0.75], "type": "io"}, {"number": 42, "name": "PB6", "xy": [-4.25, -0.25], "type": "io"}, {"number": 43, "name": "PB7", "xy": [-4.25, 0.25], "type": "io"}, {"number": 44, "name": "BOOT0", "xy": [-4.25, 0.75], "type": "ctrl"}, {"number": 45, "name": "PB8", "xy": [-4.25, 1.25], "type": "io"}, {"number": 46, "name": "PB9", "xy": [-4.25, 1.75], "type": "io"}, {"number": 47, "name": "VSS_3", "xy": [-4.25, 2.25], "type": "power"}, {"number": 48, "name": "VDD_3", "xy": [-4.25, 2.75], "type": "power"}]}
install.shadded+21@@ -0,0 +1,21 @@+#!/bin/sh+set -e+# Component Widgets is a front-end widget library + AI skill (no binary on PATH).+# Deliver the skill, widget JS, embeddable viewers, and example data into the+# skills dir so the SKILL.md surfaces to the AI and the *-widget.js files are on+# hand to inline into any page's readme.html.+DEST="$HOME/.claude/skills/component-widgets"+mkdir -p "$DEST"+cp -f "$PWD/SKILL.md" "$DEST/SKILL.md"+[ -f "$PWD/README.md" ] && cp -f "$PWD/README.md" "$DEST/README.md"+rm -rf "$DEST/widgets" "$DEST/examples" "$DEST/assets"+cp -R "$PWD/widgets" "$DEST/widgets"+cp -R "$PWD/examples" "$DEST/examples"+[ -d "$PWD/assets" ] && cp -R "$PWD/assets" "$DEST/assets"++echo "installed: component-widgets -> $DEST"+echo " skill: $DEST/SKILL.md"+echo " widgets: $DEST/widgets/*.js (+ *-viewer.html)"+echo " examples: $DEST/examples/*.json"+echo "Inline a widgets/*-widget.js into a page and add its <adom-*> tag."+echo "In a wiki readme.html use inline JSON (no fetch); see the wiki-readme-html skill."
package.jsonadded+58@@ -0,0 +1,58 @@+{+ "slug": "component-widgets",+ "title": "Component Widgets",+ "type": "app",+ "version": "1.0.0",+ "description": "Reusable, dependency-free web-component widgets to drop into any component or molecule page: <adom-pinout> (datasheet-style pin diagram + synced table), <adom-heatsink-via> (exposed-pad thermal via array, only when the part has an EP), <adom-molecule> (ganged part set + nets), and <adom-chip-thumbnail> (rendered part image). Each is one custom element, self-contained (Shadow DOM, no deps), theme-aware (light + dark), fed by inline JSON or el.data, and all on one lbr-hover cross-highlight bus so a pin hovered in one lights the same pin everywhere. Symbol, footprint, and 3D stay with adom-lbr; the datasheet renders via the native viewer. Installs the widget JS, iframe-embeddable viewers, example data, and an AI skill to ~/.claude/skills/component-widgets. Live reference page renders them all as a readme.html.",+ "license": "MIT",+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "tags": [+ "widgets",+ "web-components",+ "pinout",+ "footprint",+ "molecule",+ "thumbnail",+ "component-page",+ "readme-html",+ "adom-lbr",+ "components"+ ],+ "discovery_triggers": [+ "pinout widget",+ "adom-pinout",+ "pin diagram",+ "datasheet pinout",+ "heat-sink via widget",+ "thermal via array",+ "exposed pad vias",+ "molecule widget",+ "ganged components",+ "sub-circuit view",+ "chip thumbnail widget",+ "component page widgets",+ "readme-drop-in widget",+ "drop a pinout into a page",+ "reusable component widget",+ "cross-highlight pins on a page"+ ],+ "discovery_pitch": "Reach for component-widgets when a component or molecule page needs a live pinout, an exposed-pad thermal via pattern, a ganged molecule/sub-circuit view, or a part thumbnail: each is one self-contained <adom-*> tag fed by inline JSON, theme-aware, and cross-linked with adom-lbr's symbol/footprint/3D on one hover bus.",+ "sample_prompts": [+ {+ "label": "Add a pinout",+ "prompt": "add an <adom-pinout> for the STM32F103C8T6 to this component page's readme.html"+ },+ {+ "label": "Show the thermal vias",+ "prompt": "drop the heat-sink via widget on this QFN part's page so it shows the recommended exposed-pad via array"+ },+ {+ "label": "Visualize a molecule",+ "prompt": "render this decoupling molecule as an <adom-molecule> with its parts and nets"+ }+ ]+}
page.json+54−7@@ -4,21 +4,62 @@ "type": "app", "title": "Component Widgets", "brief": "One home for the reusable, readme-instantiable component/molecule widgets (pinout, heat-sink via, chip thumbnail, molecule) — self-contained web components on the shared adom-lbr hover bus.",- "version": "0.1.0",+ "version": "1.0.0", "visibility": "private", "author": { "name": "Ray", "email": "[email protected]" },- "tags": [],- "license": "proprietary",+ "tags": [+ "widgets",+ "web-components",+ "pinout",+ "footprint",+ "molecule",+ "thumbnail",+ "component-page",+ "readme-html",+ "adom-lbr",+ "components"+ ],+ "license": "MIT", "hero": { "type": "image", "path": "screenshots/hero.png" },- "sample_prompts": [],- "discovery_triggers": [],- "discovery_pitch": null,+ "sample_prompts": [+ {+ "label": "Add a pinout",+ "prompt": "add an <adom-pinout> for the STM32F103C8T6 to this component page's readme.html"+ },+ {+ "label": "Show the thermal vias",+ "prompt": "drop the heat-sink via widget on this QFN part's page so it shows the recommended exposed-pad via array"+ },+ {+ "label": "Visualize a molecule",+ "prompt": "render this decoupling molecule as an <adom-molecule> with its parts and nets"+ }+ ],+ "discovery_triggers": [+ "pinout widget",+ "adom-pinout",+ "pin diagram",+ "datasheet pinout",+ "heat-sink via widget",+ "thermal via array",+ "exposed pad vias",+ "molecule widget",+ "ganged components",+ "sub-circuit view",+ "chip thumbnail widget",+ "component page widgets",+ "readme-drop-in widget",+ "drop a pinout into a page",+ "reusable component widget",+ "cross-highlight pins on a page"+ ],+ "discovery_pitch": "Reach for component-widgets when a component or molecule page needs a live pinout, an exposed-pad thermal via pattern, a ganged molecule/sub-circuit view, or a part thumbnail: each is one self-contained <adom-*> tag fed by inline JSON, theme-aware, and cross-linked with adom-lbr's symbol/footprint/3D on one hover bus.", "metadata": {}, "created_at": "2026-08-06T15:51:32.326Z", "updated_at": "2026-08-06T15:51:32.326Z",@@ -29,5 +70,11 @@ "version_cmd": "" }, "skills": [],- "org": "adom"+ "org": "adom",+ "description": "Reusable, dependency-free web-component widgets to drop into any component or molecule page: <adom-pinout> (datasheet-style pin diagram + synced table), <adom-heatsink-via> (exposed-pad thermal via array, only when the part has an EP), <adom-molecule> (ganged part set + nets), and <adom-chip-thumbnail> (rendered part image). Each is one custom element, self-contained (Shadow DOM, no deps), theme-aware (light + dark), fed by inline JSON or el.data, and all on one lbr-hover cross-highlight bus so a pin hovered in one lights the same pin everywhere. Symbol, footprint, and 3D stay with adom-lbr; the datasheet renders via the native viewer. Installs the widget JS, iframe-embeddable viewers, example data, and an AI skill to ~/.claude/skills/component-widgets. Live reference page renders them all as a readme.html.",+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ } }
uninstall.shadded+3@@ -0,0 +1,3 @@+#!/bin/sh+rm -rf "$HOME/.claude/skills/component-widgets"+echo "uninstalled: component-widgets"
widgets/chip-thumbnail-widget.jsadded+38@@ -0,0 +1,38 @@+/*!+ * <adom-chip-thumbnail> — a small rendered image of the actual part.+ * <adom-chip-thumbnail src="thumb.png" mpn="74HC00" package="SOIC-14"></adom-chip-thumbnail>+ * el.data = { src, mpn, package }+ * `src` is a rendered PNG (in production: glb2png / the chip-thumbnailer off the part's GLB).+ * Self-contained, theme-aware.+ */+(() => {+ const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));+ const CSS = `+ :host{display:inline-block;--bg:#0d1420;--line:#26324a;--text:#dbe2ec;--dim:#8b97ac;--accent:#00e6dc}+ @media (prefers-color-scheme:light){:host{--bg:#fff;--line:#dbe2ee;--text:#1a2230;--dim:#5a6678;--accent:#0d8f88}}+ :host([theme="dark"]){--bg:#0d1420;--line:#26324a;--text:#dbe2ec;--dim:#8b97ac;--accent:#00e6dc}+ :host([theme="light"]){--bg:#fff;--line:#dbe2ee;--text:#1a2230;--dim:#5a6678;--accent:#0d8f88}+ *{box-sizing:border-box}+ .card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:12px;width:200px;text-align:center;color:var(--text);font:14px -apple-system,'Segoe UI',Roboto,sans-serif}+ img{width:100%;height:auto;border-radius:8px;display:block}+ .ph{aspect-ratio:1;display:flex;align-items:center;justify-content:center;color:var(--dim);border:1px dashed var(--line);border-radius:8px;font:12px ui-monospace,monospace}+ figcaption{margin-top:9px;font:600 13px ui-monospace,'SF Mono',Menlo,monospace}+ figcaption span{color:var(--accent);font-weight:400;font-size:11px}`;+ class AdomChipThumbnail extends HTMLElement {+ static get observedAttributes() { return ['src', 'mpn', 'package']; }+ set data(d) { this._data = d; if (this.shadowRoot) this._render(); }+ connectedCallback() { if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' }); this._render(); }+ attributeChangedCallback() { if (this.shadowRoot) this._render(); }+ _render() {+ const d = this._data || {};+ const src = d.src || this.getAttribute('src') || '';+ const mpn = d.mpn || this.getAttribute('mpn') || '';+ const pkg = d.package || this.getAttribute('package') || '';+ this.shadowRoot.innerHTML = `<style>${CSS}</style><figure class="card" style="margin:0">+ ${src ? `<img src="${esc(src)}" alt="${esc(mpn)}" onerror="this.outerHTML='<div class=ph>no render</div>'">` : `<div class="ph">no render</div>`}+ ${(mpn || pkg) ? `<figcaption>${esc(mpn)}${pkg ? ` · <span>${esc(pkg)}</span>` : ''}</figcaption>` : ''}+ </figure>`;+ }+ }+ if (!customElements.get('adom-chip-thumbnail')) customElements.define('adom-chip-thumbnail', AdomChipThumbnail);+})();
widgets/heatsink-via-widget.jsadded+152@@ -0,0 +1,152 @@+/*!+ * <adom-heatsink-via> — the exposed-pad thermal-via widget.+ * Only renders content when the part actually HAS an exposed/thermal pad; otherwise+ * it shows an empty state ("no exposed pad — heat-sink vias not applicable").+ * Draws the EP copper + a standard thermal via array (annular ring + drill), with+ * the via count / drill / pitch, so a page author can show the recommended+ * heat-sinking pattern for that chip.+ *+ * <script src="heatsink-via-widget.js"></script>+ * <adom-heatsink-via src="pads.json"></adom-heatsink-via>+ * el.data = { mpn, package, pads:[{number,x_mm,y_mm}] } // detects the EP+ * el.data = { mpn, package, ep:{w_mm,h_mm,number?}, via?:{drill_mm,pitch_mm} } // explicit+ *+ * Shares the adom-lbr cross-highlight bus: highlights the EP when its pin is hovered+ * elsewhere, and posts on EP hover.+ */+(() => {+ const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));+ const isCenterName = p => /^(EP|EPAD|PAD|TAB|THERMAL|E[-_]?PAD|EXP|EXPPAD)$/i.test(String(p.name || p.number || '').trim());++ // Work out the exposed pad from whatever data we were given.+ function resolveEP(d) {+ if (d.ep && (d.ep.w_mm || d.ep.h_mm)) return { w: d.ep.w_mm || d.ep.h_mm, h: d.ep.h_mm || d.ep.w_mm, number: d.ep.number };+ const m = /EP([\d.]+)x([\d.]+)/i.exec(d.package || '');+ let epNum;+ const pads = Array.isArray(d.pads) ? d.pads.filter(p => typeof p.x_mm === 'number') : [];+ if (pads.length) {+ const xs = pads.map(p => p.x_mm), ys = pads.map(p => p.y_mm);+ const minx = Math.min(...xs), maxx = Math.max(...xs), miny = Math.min(...ys), maxy = Math.max(...ys);+ const span = Math.max(maxx - minx, maxy - miny) || 1, em = 0.2 * span;+ const cenP = pads.find(p => isCenterName(p) || Math.min(Math.abs(p.x_mm - minx), Math.abs(p.x_mm - maxx), Math.abs(p.y_mm - miny), Math.abs(p.y_mm - maxy)) > em);+ if (cenP) epNum = cenP.number;+ if (m) return { w: +m[1], h: +m[2], number: epNum, bodyW: maxx - minx, bodyH: maxy - miny };+ if (cenP) { const bw = maxx - minx, bh = maxy - miny; return { w: bw * 0.55, h: bh * 0.55, number: epNum, bodyW: bw, bodyH: bh, estimated: true }; }+ }+ if (m) return { w: +m[1], h: +m[2] };+ return null;+ }++ class AdomHeatsinkVia extends HTMLElement {+ set data(d) { this._data = d; if (this.shadowRoot) this._render(); }+ get data() { return this._data; }++ async connectedCallback() {+ if (this._init) return; this._init = true;+ this.attachShadow({ mode: 'open' });+ this._busInit();+ if (!this._data) {+ const inline = this.querySelector('script[type="application/json"]');+ if (inline) { try { this._data = JSON.parse(inline.textContent); } catch (e) { /* ignore */ } }+ const src = this.getAttribute('src');+ if (!this._data && src) { try { this._data = await (await fetch(src)).json(); } catch (e) { this._err = 'could not load ' + src; } }+ }+ this._render();+ }+ _busInit() {+ this._self = 'h' + Math.random().toString(36).slice(2);+ this._bus = ('BroadcastChannel' in window) ? new BroadcastChannel('lbr-hover') : null;+ const on = e => { const x = e.data || {}; if (x.type === 'lbr-hover' && x.src !== this._self) this._hi(x.pin); };+ if (this._bus) this._bus.onmessage = on;+ this._winmsg = on; window.addEventListener('message', on);+ }+ disconnectedCallback() { try { this._bus && this._bus.close(); } catch (_) { /*noop*/ } if (this._winmsg) window.removeEventListener('message', this._winmsg); }+ _hi(n) { const g = this.shadowRoot && this.shadowRoot.querySelector('.epwrap'); if (g) g.classList.toggle('hot', n != null && this._epNum != null && String(n) === String(this._epNum)); }+ _post(n) {+ const m = { type: 'lbr-hover', pin: n == null ? null : String(n), mpn: (this._data && this._data.mpn) || '', src: this._self };+ try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }+ try { if (window.parent && window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }+ }++ _render() {+ const d = this._data || {}, root = this.shadowRoot;+ const ep = d.pads || d.ep || d.package ? resolveEP(d) : null;+ if (this._err) return void (root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="empty">${esc(this._err)}</div></div>`);+ if (!ep) {+ this._epNum = null;+ root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="hd"><span class="mpn">${esc(d.mpn || '')}</span>${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}</div>+ <div class="empty"><b>No exposed pad</b> on this package — heat-sink vias aren't applicable.</div></div>`;+ return;+ }+ this._epNum = ep.number;+ const via = { drill: (d.via && d.via.drill_mm) || 0.3, pitch: (d.via && d.via.pitch_mm) || Math.max(0.8, Math.min(1.2, Math.min(ep.w, ep.h) / 4)) };+ // via grid centred on the EP+ const margin = Math.max(0.35, via.drill * 1.2);+ const nx = Math.max(1, Math.floor((ep.w - 2 * margin) / via.pitch) + 1);+ const ny = Math.max(1, Math.floor((ep.h - 2 * margin) / via.pitch) + 1);+ const count = nx * ny;+ root.innerHTML = `<style>${this._css()}</style>+ <div class="card">+ <div class="hd"><span class="mpn">${esc(d.mpn || '')}</span>${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">exposed pad</span></div>+ <div class="body">+ ${this._svg(ep, via, nx, ny)}+ <div class="info">+ <div class="kv"><span>Exposed pad</span><b>${ep.w.toFixed(2)} × ${ep.h.toFixed(2)} mm${ep.estimated ? ' *' : ''}</b></div>+ <div class="kv"><span>Thermal vias</span><b>${count} (${nx}×${ny})</b></div>+ <div class="kv"><span>Drill · pitch</span><b>Ø${via.drill} · ${via.pitch.toFixed(2)} mm</b></div>+ <p class="note">A grid of plated vias under the exposed pad carries heat from the die into the board's inner/bottom copper (usually a ground plane). Tent or plug them to stop solder wicking during reflow.${ep.estimated ? '<br><span class="star">* EP size estimated from the footprint; use the datasheet land pattern for production.</span>' : ''}</p>+ </div>+ </div>+ </div>`;+ const w = root.querySelector('.epwrap');+ if (w) { w.onmouseenter = () => { this._hi(ep.number); this._post(ep.number); }; w.onmouseleave = () => { this._hi(null); this._post(null); }; }+ }++ _svg(ep, via, nx, ny) {+ const S = 300, pad = 34;+ const bodyW = ep.bodyW || ep.w * 1.5, bodyH = ep.bodyH || ep.h * 1.5;+ const span = Math.max(bodyW, bodyH) || 1, sc = (S - 2 * pad) / span, c = S / 2;+ const X = mm => c + mm * sc, Y = mm => c + mm * sc;+ const epW = ep.w * sc, epH = ep.h * sc, bW = bodyW * sc, bH = bodyH * sc;+ let s = `<svg viewBox="0 0 ${S} ${S}" class="hv">`;+ s += `<rect class="courtyard" x="${c - bW / 2}" y="${c - bH / 2}" width="${bW}" height="${bH}" rx="6"/>`;+ s += `<g class="epwrap"><rect class="ep" x="${c - epW / 2}" y="${c - epH / 2}" width="${epW}" height="${epH}" rx="2"/>`;+ const x0 = -(nx - 1) * via.pitch / 2, y0 = -(ny - 1) * via.pitch / 2;+ const vr = Math.max(3, via.drill * sc * 0.9), dr = Math.max(1.4, via.drill * sc * 0.5);+ for (let i = 0; i < nx; i++) for (let j = 0; j < ny; j++) {+ const vx = X(x0 + i * via.pitch), vy = Y(y0 + j * via.pitch);+ s += `<circle class="viaring" cx="${vx}" cy="${vy}" r="${vr}"/><circle class="viadrill" cx="${vx}" cy="${vy}" r="${dr}"/>`;+ }+ s += `</g>`;+ s += `<text x="${c}" y="${c - bH / 2 - 8}" text-anchor="middle" class="lbl">exposed pad · ${nx}×${ny} thermal vias</text>`;+ return s + `</svg>`;+ }++ _css() {+ return `+ :host{display:block;--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;--copper:#e0913a;--drill:#0a0e14}+ @media (prefers-color-scheme:light){:host{--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--copper:#c47a24;--drill:#fff}}+ :host([theme="dark"]){--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;--copper:#e0913a;--drill:#0a0e14}+ :host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--copper:#c47a24;--drill:#fff}+ *{box-sizing:border-box}+ .card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);font:14px -apple-system,'Segoe UI',Roboto,Arial,sans-serif}+ .hd{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}+ .hd .mpn{font:700 16px ui-monospace,'SF Mono',Menlo,monospace} .hd .pkg{font:600 11px ui-monospace,monospace;color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);padding:2px 8px;border-radius:6px}+ .hd .count{margin-left:auto;font:11px ui-monospace,monospace;color:var(--copper);text-transform:uppercase;letter-spacing:.08em}+ .body{display:grid;grid-template-columns:minmax(220px,1fr) minmax(200px,1fr);gap:18px;align-items:center}+ @media(max-width:560px){.body{grid-template-columns:1fr}}+ svg.hv{width:100%;height:auto;max-height:300px;display:block;background:var(--panel);border:1px solid var(--line);border-radius:10px}+ .courtyard{fill:none;stroke:var(--line2);stroke-width:1.2;stroke-dasharray:3 3}+ .ep{fill:var(--copper);fill-opacity:.22;stroke:var(--copper);stroke-width:1.4}+ .viaring{fill:var(--copper);fill-opacity:.9} .viadrill{fill:var(--drill)}+ .epwrap{cursor:default} .epwrap.hot .ep{fill-opacity:.4;stroke-width:2.4;filter:drop-shadow(0 0 5px var(--copper))}+ .epwrap.hot .viaring{fill-opacity:1}+ .lbl{fill:var(--dim);font:9px ui-monospace,monospace}+ .info .kv{display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);font-size:13px}+ .info .kv span{color:var(--dim2)} .info .kv b{font-family:ui-monospace,monospace}+ .info .note{font-size:12px;color:var(--dim);margin:10px 0 0;line-height:1.5} .info .star{color:var(--dim2);font-size:11px}+ .empty{color:var(--dim);padding:22px;text-align:center;font-size:13.5px} .empty b{color:var(--text)}`;+ }+ }+ if (!customElements.get('adom-heatsink-via')) customElements.define('adom-heatsink-via', AdomHeatsinkVia);+})();
widgets/heatsink-viewer.htmladded+33@@ -0,0 +1,33 @@+<!doctype html>+<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">+<title>Heat-sink via viewer</title>+<style>html,body{margin:0;background:transparent}adom-heatsink-via{display:block}.msg{padding:22px;color:#8b97ac;font:13px/1.6 ui-monospace,monospace}</style></head>+<body>+<!-- Embeddable heat-sink-via viewer. ?part=<mpn>&api=<reef base> (pads) · ?src=<json> · postMessage {type:'heatsink',data} · ?theme= -->+<adom-heatsink-via id="w"></adom-heatsink-via>+<div class="msg" id="msg" hidden></div>+<script src="heatsink-via-widget.js"></script>+<script>+const w=document.getElementById('w'), msg=document.getElementById('msg'), P=new URLSearchParams(location.search);+const REEF=(P.get('api')||'').replace(/\/$/,'');+const show=t=>{msg.hidden=false; msg.textContent=t;};+async function loadPart(key){+ if(!REEF) return show('?part= needs a data source: pass &api=<reef base>, or use ?src=<json-url>');+ try{+ const d=await (await fetch(REEF+'/reef/page/'+encodeURIComponent(key))).json();+ if(!d.resolved) return show('no match for '+key);+ const v=d.variants[d.primary||0], pkgKey=(v.packageOptions[0]||{}).package_key, pkgClass=(v.packageOptions[0]||{}).package_class||'';+ const pk=await (await fetch(REEF+'/api/package/'+encodeURIComponent(pkgKey))).json();+ w.data={ mpn:v.name, package:pk.package_key||pkgClass, pads:Array.isArray(pk.pads)?pk.pads:[] };+ }catch(e){ show('error: '+e); }+}+async function loadSrc(url){ try{ w.data=await (await fetch(url)).json(); }catch(e){ show('error: '+e); } }+if(P.get('theme')) w.setAttribute('theme', P.get('theme'));+if(P.get('part')) loadPart(P.get('part'));+else if(P.get('src')) loadSrc(P.get('src'));+else show('pass ?src=<json-url> (or ?part=<mpn>&api=<reef base>), or postMessage {type:"heatsink", data:{…}}');+addEventListener('message', e=>{ const m=e.data||{}; if(m.type==='heatsink' && m.data){ msg.hidden=true; w.data=m.data; } });+function ph(){ try{ parent.postMessage({type:'readme-height', height:Math.max(120,Math.min(16000,document.body.scrollHeight||200))}, '*'); }catch(_){} }+addEventListener('load',ph); setTimeout(ph,300); if('ResizeObserver' in window) new ResizeObserver(ph).observe(document.body);+</script>+</body></html>
widgets/molecule-viewer.htmladded+21@@ -0,0 +1,21 @@+<!doctype html>+<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">+<title>Molecule viewer</title>+<style>html,body{margin:0;background:transparent}adom-molecule{display:block}.msg{padding:22px;color:#8b97ac;font:13px/1.6 ui-monospace,monospace}</style></head>+<body>+<!-- Embeddable ganged-molecule viewer. ?src=<json> · postMessage {type:'molecule',data} · ?theme= -->+<adom-molecule id="w"></adom-molecule>+<div class="msg" id="msg" hidden></div>+<script src="molecule-widget.js"></script>+<script>+const w=document.getElementById('w'), msg=document.getElementById('msg'), P=new URLSearchParams(location.search);+const show=t=>{msg.hidden=false; msg.textContent=t;};+async function loadSrc(url){ try{ w.data=await (await fetch(url)).json(); }catch(e){ show('error: '+e); } }+if(P.get('theme')) w.setAttribute('theme', P.get('theme'));+if(P.get('src')) loadSrc(P.get('src'));+else show('pass ?src=<json-url>, or postMessage {type:"molecule", data:{…}}');+addEventListener('message', e=>{ const m=e.data||{}; if(m.type==='molecule' && m.data){ msg.hidden=true; w.data=m.data; } });+function ph(){ try{ parent.postMessage({type:'readme-height', height:Math.max(120,Math.min(16000,document.body.scrollHeight||200))}, '*'); }catch(_){} }+addEventListener('load',ph); setTimeout(ph,300); if('ResizeObserver' in window) new ResizeObserver(ph).observe(document.body);+</script>+</body></html>
widgets/molecule-widget.jsadded+113@@ -0,0 +1,113 @@+/*!+ * <adom-molecule> — the ganged-component widget for a molecule (a reusable sub-circuit).+ * Shows the molecule's parts as a ganged set + how they connect (nets). Hover a net to+ * light every part on it; hover a part to light its nets — the "ganged" relationship.+ *+ * <adom-molecule src="molecule.json"></adom-molecule>+ * el.data = {+ * name, mpn?,+ * components: [ { ref, mpn, value?, package?, type? } ], // type: mcu|cap|res|xtal|ic|conn|diode|other+ * nets: [ { name, pins: [ {ref, pin} ] } ]+ * }+ * Self-contained, theme-aware, shares the adom-lbr lbr-hover bus (posts a part's ref).+ */+(() => {+ const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));+ const TC = { mcu: '#b57cff', ic: '#4c9dff', cap: '#00d3c8', res: '#ffb454', xtal: '#4c9dff', diode: '#ff5c5c', conn: '#e0913a', other: '#8b97ac' };+ const TLAB = { mcu: 'MCU', ic: 'IC', cap: 'cap', res: 'res', xtal: 'xtal', diode: 'diode', conn: 'conn', other: '' };+ function inferType(c) {+ if (c.type) return c.type;+ const r = String(c.ref || '').toUpperCase();+ if (r[0] === 'U') return /STM|MCU|ESP|ATMEGA|RP2/i.test(c.mpn || '') ? 'mcu' : 'ic';+ if (r[0] === 'C') return 'cap'; if (r[0] === 'R') return 'res'; if (r[0] === 'Y' || r[0] === 'X') return 'xtal';+ if (r[0] === 'D') return 'diode'; if (r[0] === 'J' || r[0] === 'P') return 'conn';+ return 'other';+ }++ class AdomMolecule extends HTMLElement {+ set data(d) { this._data = d; if (this.shadowRoot) this._render(); }+ get data() { return this._data; }+ async connectedCallback() {+ if (this._init) return; this._init = true;+ this.attachShadow({ mode: 'open' });+ this._self = 'm' + Math.random().toString(36).slice(2);+ this._bus = ('BroadcastChannel' in window) ? new BroadcastChannel('lbr-hover') : null;+ if (!this._data) {+ const inl = this.querySelector('script[type="application/json"]');+ if (inl) { try { this._data = JSON.parse(inl.textContent); } catch (e) { /* */ } }+ const src = this.getAttribute('src');+ if (!this._data && src) { try { this._data = await (await fetch(src)).json(); } catch (e) { this._err = 'could not load ' + src; } }+ }+ this._render();+ }+ disconnectedCallback() { try { this._bus && this._bus.close(); } catch (_) { /* */ } }++ _render() {+ const d = this._data, root = this.shadowRoot;+ if (this._err || !d || !Array.isArray(d.components)) {+ root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="empty">${esc(this._err || 'no molecule data')}</div></div>`;+ return;+ }+ const comps = d.components.map(c => ({ ...c, type: inferType(c) }));+ const nets = Array.isArray(d.nets) ? d.nets : [];+ const netOf = new Map(); // ref -> [net names]+ nets.forEach(n => (n.pins || []).forEach(p => { const a = netOf.get(p.ref) || []; if (!a.includes(n.name)) a.push(n.name); netOf.set(p.ref, a); }));+ root.innerHTML = `<style>${this._css()}</style>+ <div class="card">+ <div class="hd"><span class="mpn">${esc(d.name || d.mpn || 'molecule')}</span><span class="count">${comps.length} parts · ${nets.length} nets</span></div>+ <div class="readout" data-readout>hover a part or net →</div>+ <div class="gang">${comps.map(c => `+ <div class="part" data-ref="${esc(c.ref)}" style="--c:${TC[c.type] || TC.other}">+ <span class="ref">${esc(c.ref)}</span>+ <span class="val">${esc(c.value || c.mpn || '')}</span>+ <span class="meta">${esc(c.package || '')}${TLAB[c.type] ? ' · ' + TLAB[c.type] : ''}</span>+ </div>`).join('')}</div>+ <div class="nets">${nets.map(n => `<div class="net" data-net="${esc(n.name)}" data-refs="${esc((n.pins || []).map(p => p.ref).join(','))}">+ <span class="nn">${esc(n.name)}</span><span class="nc">${(n.pins || []).length} pins</span>+ <span class="nr">${esc([...new Set((n.pins || []).map(p => p.ref))].join(' '))}</span></div>`).join('')}</div>+ </div>`;+ this._wire(netOf);+ }++ _wire(netOf) {+ const root = this.shadowRoot, ro = root.querySelector('[data-readout]');+ const parts = [...root.querySelectorAll('.part')], netEls = [...root.querySelectorAll('.net')];+ const clear = () => { parts.forEach(p => p.classList.remove('hot', 'dim')); netEls.forEach(n => n.classList.remove('hot', 'dim')); ro.innerHTML = 'hover a part or net →'; this._post(null); };+ const litParts = refs => { const set = new Set(refs); parts.forEach(p => { p.classList.toggle('hot', set.has(p.dataset.ref)); p.classList.toggle('dim', !set.has(p.dataset.ref)); }); };+ const litNets = names => { const set = new Set(names); netEls.forEach(n => { n.classList.toggle('hot', set.has(n.dataset.net)); n.classList.toggle('dim', !set.has(n.dataset.net)); }); };+ netEls.forEach(n => {+ n.onmouseenter = () => { const refs = (n.dataset.refs || '').split(',').filter(Boolean); litParts(refs); n.classList.add('hot'); netEls.forEach(x => x !== n && x.classList.add('dim')); ro.innerHTML = `<b>${esc(n.dataset.net)}</b> · ${refs.length} pins on ${new Set(refs).size} parts`; };+ n.onmouseleave = clear;+ });+ parts.forEach(p => {+ p.onmouseenter = () => { const names = netOf.get(p.dataset.ref) || []; litNets(names); p.classList.add('hot'); parts.forEach(x => x !== p && x.classList.add('dim')); ro.innerHTML = `<b>${esc(p.dataset.ref)}</b> · on ${names.length ? names.join(', ') : 'no nets'}`; this._post(p.dataset.ref); };+ p.onmouseleave = clear;+ });+ }+ _post(ref) { const m = { type: 'lbr-hover', ref: ref, mpn: (this._data && this._data.name) || '', src: this._self }; try { this._bus && this._bus.postMessage(m); } catch (_) { /* */ } try { if (window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /* */ } }++ _css() {+ return `+ :host{display:block;--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc}+ @media (prefers-color-scheme:light){:host{--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88}}+ :host([theme="dark"]){--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc}+ :host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88}+ *{box-sizing:border-box}+ .card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);font:14px -apple-system,'Segoe UI',Roboto,Arial,sans-serif}+ .hd{display:flex;align-items:baseline;gap:10px;margin-bottom:8px}+ .hd .mpn{font:700 16px ui-monospace,'SF Mono',Menlo,monospace} .hd .count{margin-left:auto;font:12px ui-monospace,monospace;color:var(--dim2)}+ .readout{height:22px;display:flex;align-items:center;font:600 12.5px ui-monospace,monospace;color:var(--dim);margin-bottom:10px}+ .readout b{color:var(--accent)}+ .gang{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}+ .part{border:1px solid var(--line2);border-left:3px solid var(--c);border-radius:9px;padding:7px 11px;min-width:88px;background:var(--panel);transition:opacity .1s}+ .part .ref{font:700 12px ui-monospace,monospace;color:var(--c)} .part .val{display:block;font:600 12.5px ui-monospace,monospace;margin-top:1px} .part .meta{display:block;font-size:10.5px;color:var(--dim2);margin-top:1px}+ .part.hot{box-shadow:0 0 0 1px var(--c);background:color-mix(in srgb,var(--c) 10%,var(--panel))} .part.dim{opacity:.32}+ .nets{border-top:1px solid var(--line);padding-top:8px}+ .net{display:flex;align-items:center;gap:10px;padding:4px 8px;border-radius:6px;font:12px ui-monospace,monospace;cursor:default}+ .net .nn{color:var(--text);font-weight:700;min-width:70px} .net .nc{color:var(--dim2);font-size:11px} .net .nr{color:var(--dim);margin-left:auto;font-size:11px;text-align:right}+ .net.hot{background:color-mix(in srgb,var(--accent) 12%,transparent)} .net.hot .nn{color:var(--accent)} .net.dim{opacity:.34}+ .empty{color:var(--dim2);padding:26px;text-align:center;font-family:ui-monospace,monospace}`;+ }+ }+ if (!customElements.get('adom-molecule')) customElements.define('adom-molecule', AdomMolecule);+})();
widgets/pinout-viewer.htmladded+55@@ -0,0 +1,55 @@+<!doctype html>+<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">+<title>Pinout viewer</title>+<style>+ html,body{margin:0;background:transparent}+ adom-pinout{display:block}+ .msg{padding:22px;color:#8b97ac;font:13px/1.6 ui-monospace,'JetBrains Mono',monospace}+</style></head>+<body>+<!--+ Embeddable per-slot pinout viewer (wiki lbr-slot / modern-LBR pattern).+ Drive it three ways:+ ?part=<mpn|symbolKey>&api=<reef-base> → fetch <api>/reef/page and render+ ?src=<json-url> → fetch the pin-data JSON and render+ postMessage {type:'pinout', data:{...}} → host feeds data directly+ Cross-slot linking:+ posts {type:'pinout-pinhover', pin} to the parent on hover+ accepts {type:'pinout-highlight', pin} from the parent to drive the highlight+ ?theme=light|dark+-->+<adom-pinout id="w"></adom-pinout>+<div class="msg" id="msg" hidden></div>+<script src="pinout-widget.js"></script>+<script>+const w=document.getElementById('w'), msg=document.getElementById('msg'), P=new URLSearchParams(location.search);+const REEF=(P.get('api')||'').replace(/\/$/,'');+const show=t=>{msg.hidden=false; msg.textContent=t;};+async function loadPart(key){+ if(!REEF) return show('?part= needs a data source: pass &api=<reef base>, or use ?src=<json-url>');+ try{+ const d=await (await fetch(REEF+'/reef/page/'+encodeURIComponent(key))).json();+ if(!d.resolved) return show('no match for '+key);+ const v=d.variants[d.primary||0];+ w.data={ mpn:v.name, package:(v.packageOptions[0]||{}).package_class||'', pins:(v.pinList||[]).map(p=>({number:p.number,name:p.name})) };+ }catch(e){ show('error: '+e); }+}+async function loadSrc(url){ try{ w.data=await (await fetch(url)).json(); }catch(e){ show('error: '+e); } }++if(P.get('theme')) w.setAttribute('theme', P.get('theme'));+if(P.get('part')) loadPart(P.get('part'));+else if(P.get('src')) loadSrc(P.get('src'));+else show('pass ?src=<json-url> (or ?part=<mpn>&api=<reef base>), or postMessage {type:"pinout", data:{…}}');++// cross-slot linking (symbol ↔ footprint ↔ 3D ↔ pinout)+addEventListener('message', e=>{+ const m=e.data||{};+ if(m.type==='pinout' && m.data){ msg.hidden=true; w.data=m.data; }+ else if(m.type==='pinout-highlight' && w.highlight){ w.highlight(m.pin); }+});+w.addEventListener('pinhover', e=>{ try{ parent.postMessage({type:'pinout-pinhover', pin:e.detail?e.detail.number:null}, '*'); }catch(_){} });+// auto-grow the host readme iframe+function ph(){ try{ parent.postMessage({type:'readme-height', height:Math.max(120,Math.min(16000,document.body.scrollHeight||200))}, '*'); }catch(_){} }+addEventListener('load',ph); setTimeout(ph,300); if('ResizeObserver' in window) new ResizeObserver(ph).observe(document.body);+</script>+</body></html>
widgets/pinout-widget.jsadded+256@@ -0,0 +1,256 @@+/*!+ * <adom-pinout> — a reusable, dependency-free pinout widget.+ *+ * Drop-in:+ * <script src="pinout-widget.js"></script>+ * <adom-pinout src="part.json"></adom-pinout>+ * <adom-pinout><script type="application/json">{ ...pin data... }</script></adom-pinout>+ * // or: document.querySelector('adom-pinout').data = { ... }+ *+ * Data contract (everything except pins[].name is optional):+ * {+ * "mpn": "STM32F103C8T6", // label shown in the chip body+ * "package": "LQFP-48", // label + hints the inferred layout (quad vs dual)+ * "pins": [+ * { "number": 1, "name": "VBAT", // number+name are the essentials+ * "type": "power", // power|io|clock|ctrl|other (auto-inferred from name if omitted)+ * "xy": [-2.75, 4.25] } // mm pad centre (footprint frame, +y up) — best placement.+ * // omit xy → placed by inferred quad/dual layout.+ * ]+ * }+ *+ * Attributes: src (fetch JSON), layout ("auto"|"quad"|"dual"), heading (override title).+ * Self-contained (Shadow DOM), themes to the host via prefers-color-scheme.+ */+(() => {+ const TYPE_COLORS = {+ power: '#ff5c5c', io: '#00d3c8', clock: '#4c9dff', ctrl: '#b57cff', control: '#b57cff', other: '#8b97ac',+ };+ const TYPE_LABEL = { power: 'power', io: 'I/O', clock: 'clock', ctrl: 'control', control: 'control', other: 'other' };++ function inferType(name) {+ const n = String(name || '').toUpperCase();+ if (n === 'NC' || n === '') return 'other';+ if (/^(VDD|VSS|VCC|GND|VBAT|VDDA|VSSA|AVDD|AVSS|VREF|VEE|VBUS|V\+|V-|AGND|DGND)/.test(n)) return 'power';+ if (/(OSC|XTAL|CLKIN|CLKOUT|\bCLK\b|SCLK|MCLK)/.test(n)) return 'clock';+ if (/^(NRST|~?RESET|RST|BOOT|EN\b|~?CE|~?CS|MODE|TEST|WAKE|SHDN|~?SHDN|PDN)/.test(n)) return 'ctrl';+ return 'io';+ }++ // Exposed / thermal pad (QFN/DFN/SON/PowerPAD) — belongs in the chip centre, not on an edge.+ const isCenterPad = p => {+ const n = String(p.name || '').trim().toUpperCase();+ return /^(EP|EPAD|PAD|TAB|THERMAL|E[-_]?PAD|EXP|EXPPAD)$/.test(n) || /EXPOSED\s*PAD|THERMAL\s*PAD/.test(n);+ };++ // Return each pin as { ...pin, x, y, side } in a S×S box. Center pads get side:'center'.+ function layout(pins, pkg, forced) {+ const S = 400, pad = 60, mid = S / 2;+ const centers = pins.filter(isCenterPad);+ const edge = pins.filter(p => !isCenterPad(p));+ let laid;+ const hasXY = edge.length > 0 && edge.every(p => Array.isArray(p.xy) && p.xy.length === 2);+ if (hasXY && forced !== 'quad' && forced !== 'dual') {+ const xs = edge.map(p => p.xy[0]), ys = edge.map(p => p.xy[1]);+ const minx = Math.min(...xs), maxx = Math.max(...xs), miny = Math.min(...ys), maxy = Math.max(...ys);+ const span = Math.max(maxx - minx, maxy - miny) || 1, sc = (S - 2 * pad) / span;+ const cx = (maxx + minx) / 2, cy = (maxy + miny) / 2, off = (v, c) => mid + (v - c) * sc;+ laid = edge.map(p => {+ const x = off(p.xy[0], cx), y = S - off(p.xy[1], cy); // flip y (mm +y up → screen down)+ const side = p.xy[1] === maxy ? 'top' : p.xy[1] === miny ? 'bottom' : p.xy[0] === maxx ? 'right' : 'left';+ return { ...p, x, y, side };+ });+ } else {+ const N = edge.length, inner = pad, outer = S - pad, at = (a, b, t) => a + (b - a) * t;+ const quad = forced === 'quad' || (forced !== 'dual' && /QF|BGA|LGA|LCC|QFN|QFP/i.test(pkg || ''));+ if (quad) {+ const per = Math.ceil(N / 4);+ laid = edge.map((p, i) => {+ const s = Math.floor(i / per), t = ((i % per) + 0.5) / per;+ if (s === 0) return { ...p, x: inner, y: at(inner, outer, t), side: 'left' };+ if (s === 1) return { ...p, x: at(inner, outer, t), y: outer, side: 'bottom' };+ if (s === 2) return { ...p, x: outer, y: at(outer, inner, t), side: 'right' };+ return { ...p, x: at(outer, inner, t), y: inner, side: 'top' };+ });+ } else {+ const half = Math.ceil(N / 2); // DIP-style: left top→bottom, right bottom→top+ laid = edge.map((p, i) => i < half+ ? { ...p, x: inner, y: at(inner, outer, (i + 0.5) / half), side: 'left' }+ : { ...p, x: outer, y: at(outer, inner, (i - half + 0.5) / (N - half)), side: 'right' });+ }+ }+ centers.forEach(p => laid.push({ ...p, x: mid, y: mid, side: 'center' }));+ return laid;+ }++ const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));++ class AdomPinout extends HTMLElement {+ set data(d) { this._data = d; if (this.shadowRoot) this._render(); }+ get data() { return this._data; }++ async connectedCallback() {+ if (this._init) return; this._init = true;+ this.attachShadow({ mode: 'open' });+ this._busInit();+ if (!this._data) {+ const inline = this.querySelector('script[type="application/json"]');+ if (inline) { try { this._data = JSON.parse(inline.textContent); } catch (e) { /* ignore */ } }+ const src = this.getAttribute('src');+ if (!this._data && src) { try { this._data = await (await fetch(src)).json(); } catch (e) { this._err = 'could not load ' + src; } }+ }+ this._render();+ }++ _render() {+ const d = this._data;+ const root = this.shadowRoot;+ if (this._err || !d || !Array.isArray(d.pins)) {+ root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="empty">${esc(this._err || 'no pin data')}</div></div>`;+ return;+ }+ const pins = d.pins.map(p => ({ ...p, type: (p.type || inferType(p.name)).toLowerCase() }));+ const laid = layout(pins, d.package, this.getAttribute('layout'));+ const typesPresent = [...new Set(pins.map(p => p.type))];+ const heading = this.getAttribute('heading') || d.mpn || '';+ root.innerHTML = `<style>${this._css()}</style>+ <div class="card">+ ${heading ? `<div class="hd"><span class="mpn">${esc(heading)}</span>${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">${pins.length} pins</span></div>` : ''}+ <div class="body">+ <div class="diagram">+ <div class="readout" data-readout>hover a pin →</div>+ ${this._svg(laid, d)}+ <div class="legend">${typesPresent.map(t => `<span><i style="background:${TYPE_COLORS[t] || TYPE_COLORS.other}"></i>${TYPE_LABEL[t] || t}</span>`).join('')}</div>+ </div>+ <div class="tablewrap"><table>+ <thead><tr><th>#</th><th>Name</th><th>Type</th></tr></thead>+ <tbody>${pins.slice().sort((a, b) => a.number - b.number).map(p => `+ <tr data-pin="${esc(p.number)}"><td class="num">${esc(p.number)}</td><td class="nm">${esc(p.name)}</td>+ <td><span class="tp" style="color:${TYPE_COLORS[p.type] || TYPE_COLORS.other}">${TYPE_LABEL[p.type] || p.type}</span></td></tr>`).join('')}</tbody>+ </table></div>+ </div>+ </div>`;+ this._wire(pins);+ }++ _svg(laid, d) {+ const S = 400;+ const edge = laid.filter(p => p.side !== 'center');+ const cen = laid.filter(p => p.side === 'center');+ const xs = edge.map(p => p.x), ys = edge.map(p => p.y);+ const pminX = Math.min(...xs), pmaxX = Math.max(...xs), pminY = Math.min(...ys), pmaxY = Math.max(...ys);+ const sides = new Set(edge.map(p => p.side)), hasTB = sides.has('top') || sides.has('bottom');+ const bL = pminX + 16, bR = pmaxX - 16, bT = hasTB ? pminY + 16 : pminY - 12, bB = hasTB ? pmaxY - 16 : pmaxY + 12;+ const cx = (bL + bR) / 2, cyB = (bT + bB) / 2;+ let s = `<svg viewBox="0 0 ${S} ${S}" class="chip">`;+ s += `<rect x="${bL}" y="${bT}" width="${bR - bL}" height="${bB - bT}" rx="10" class="pkgbody"/>`;+ s += `<circle cx="${bL + 15}" cy="${bT + 15}" r="4" class="pin1"/>`;+ const lY = cen.length ? bT + 32 : cyB - 3;+ s += `<text x="${cx}" y="${lY}" text-anchor="middle" class="lbl">${esc(d.mpn || '')}</text>`;+ if (d.package) s += `<text x="${cx}" y="${lY + 13}" text-anchor="middle" class="lbl2">${esc(d.package)}</text>`;+ // center pad(s) — exposed/thermal pad, drawn in the body centre+ cen.forEach((p, i) => {+ const col = TYPE_COLORS[p.type] || TYPE_COLORS.other;+ const ex = cx + (i - (cen.length - 1) / 2) * 40, ey = cyB + (cen.length ? 22 : 0);+ s += `<g class="pin" data-pin="${esc(p.number)}">`+ + `<rect class="ep" x="${ex - 19}" y="${ey - 13}" width="38" height="26" rx="4" fill="${col}" fill-opacity="0.15" stroke="${col}"/>`+ + `<text class="ept" x="${ex}" y="${ey + 3}" text-anchor="middle">${esc(p.name)}·${esc(p.number)}</text></g>`;+ });+ for (const p of edge) {+ const col = TYPE_COLORS[p.type] || TYPE_COLORS.other;+ let ex = p.x, ey = p.y, lx = p.x, ly = p.y, anc = 'middle';+ if (p.side === 'left') { ex = bL; lx = p.x - 11; anc = 'end'; ly = p.y + 3; }+ else if (p.side === 'right') { ex = bR; lx = p.x + 11; anc = 'start'; ly = p.y + 3; }+ else if (p.side === 'top') { ey = bT; ly = p.y - 9; }+ else { ey = bB; ly = p.y + 14; }+ s += `<g class="pin" data-pin="${esc(p.number)}">`+ + `<line class="stub" x1="${p.x}" y1="${p.y}" x2="${ex}" y2="${ey}" stroke="${col}"/>`+ + `<circle class="dot" cx="${p.x}" cy="${p.y}" r="3.1" fill="${col}"/>`+ + `<text class="pn" x="${lx}" y="${ly}" text-anchor="${anc}">${esc(p.number)}</text></g>`;+ }+ return s + `</svg>`;+ }++ // Set the highlighted pin. `emit` fires a `pinhover` event (internal hover);+ // external callers use highlight() which does NOT emit (avoids feedback loops).+ _set(n, emit) {+ const root = this.shadowRoot; if (!root) return;+ const s = (n == null ? '' : String(n));+ root.querySelectorAll('tr[data-pin]').forEach(r => r.classList.toggle('hot', r.dataset.pin === s));+ root.querySelectorAll('svg .pin[data-pin]').forEach(g => g.classList.toggle('hot', g.dataset.pin === s));+ const p = (this._pinsRendered || []).find(x => String(x.number) === s);+ const ro = root.querySelector('[data-readout]');+ if (ro) ro.innerHTML = p+ ? `<b>PIN ${esc(p.number)}</b> · ${esc(p.name)}<span class="rt" style="color:${TYPE_COLORS[p.type] || TYPE_COLORS.other}">${TYPE_LABEL[p.type] || p.type}</span>`+ : 'hover a pin →';+ if (emit) { this.dispatchEvent(new CustomEvent('pinhover', { detail: p || null, bubbles: true })); this._emitBus(s); }+ }++ /** Public: drive the highlight from outside (e.g. a linked footprint/symbol widget). */+ highlight(n) { this._set(n, false); }++ // adom-lbr shared cross-highlight bus: BroadcastChannel('lbr-hover') + parent relay.+ _busInit() {+ this._self = 'w' + Math.random().toString(36).slice(2);+ this._bus = ('BroadcastChannel' in window) ? new BroadcastChannel('lbr-hover') : null;+ const on = e => { const d = e.data || {}; if (d.type === 'lbr-hover' && d.src !== this._self) this._set(d.pin == null ? '' : d.pin, false); };+ if (this._bus) this._bus.onmessage = on;+ this._winmsg = on; window.addEventListener('message', on);+ }+ disconnectedCallback() { try { this._bus && this._bus.close(); } catch (_) { /*noop*/ } if (this._winmsg) window.removeEventListener('message', this._winmsg); }+ _emitBus(n) {+ const m = { type: 'lbr-hover', pin: (n === '' || n == null) ? null : String(n), mpn: (this._data && this._data.mpn) || '', src: this._self };+ try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }+ try { if (window.parent && window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }+ }++ _wire(pins) {+ this._pinsRendered = pins;+ const root = this.shadowRoot;+ root.querySelectorAll('tr[data-pin]').forEach(r => { r.onmouseenter = () => this._set(r.dataset.pin, true); r.onmouseleave = () => this._set('', true); });+ root.querySelectorAll('svg .pin[data-pin]').forEach(g => { g.onmouseenter = () => this._set(g.dataset.pin, true); g.onmouseleave = () => this._set('', true); });+ }++ _css() {+ return `+ :host{display:block;+ --bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;+ --hot:rgba(0,230,220,.13)}+ @media (prefers-color-scheme:light){:host{--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--hot:rgba(13,143,136,.10)}}+ :host([theme="dark"]){--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;--hot:rgba(0,230,220,.13)}+ :host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--hot:rgba(13,143,136,.10)}+ *{box-sizing:border-box}+ .card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);+ font:14px/1.5 -apple-system,'Segoe UI',Roboto,Arial,sans-serif}+ .hd{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}+ .hd .mpn{font:700 17px ui-monospace,'SF Mono',Menlo,monospace;letter-spacing:-.2px}+ .hd .pkg{font:600 11px ui-monospace,monospace;color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);padding:2px 8px;border-radius:6px}+ .hd .count{margin-left:auto;font:12px ui-monospace,monospace;color:var(--dim2)}+ .body{display:grid;grid-template-columns:minmax(300px,1fr) minmax(220px,1fr);gap:20px;align-items:start}+ @media(max-width:620px){.body{grid-template-columns:1fr}}+ .readout{height:24px;display:flex;align-items:center;gap:9px;font:600 13px ui-monospace,monospace;color:var(--dim);padding:0 4px}+ .readout b{color:var(--accent)} .readout .rt{margin-left:auto;font-size:11px;text-transform:uppercase;letter-spacing:.08em}+ .diagram{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px}+ svg.chip{width:100%;height:auto;display:block;max-height:440px}+ .pkgbody{fill:color-mix(in srgb,var(--text) 6%,var(--panel));stroke:var(--line2);stroke-width:1}+ .pin1{fill:var(--accent)}+ .lbl{fill:var(--dim);font:600 11px ui-monospace,monospace} .lbl2{fill:var(--dim2);font:9px ui-monospace,monospace}+ .pin .stub{stroke-width:2} .pin .pn{fill:var(--dim2);font:8px ui-monospace,monospace}+ .pin:hover .stub,.pin.hot .stub{stroke-width:3.6} .pin:hover .pn,.pin.hot .pn{fill:var(--text);font-weight:700}+ .pin .ep{stroke-width:1.2} .pin .ept{fill:var(--dim);font:8px ui-monospace,monospace}+ .pin:hover .ep,.pin.hot .ep{fill-opacity:.34;stroke-width:2} .pin:hover .ept,.pin.hot .ept{fill:var(--text);font-weight:700}+ .pin{cursor:default}+ .legend{display:flex;flex-wrap:wrap;gap:13px;margin:8px 2px 2px;font-size:11.5px;color:var(--dim)}+ .legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;vertical-align:middle}+ .tablewrap{max-height:440px;overflow:auto;border:1px solid var(--line);border-radius:10px}+ table{width:100%;border-collapse:collapse;font-size:12.5px}+ thead th{position:sticky;top:0;background:var(--panel);text-align:left;color:var(--dim2);font:600 10px ui-monospace,monospace;letter-spacing:.08em;text-transform:uppercase;padding:7px 9px;border-bottom:1px solid var(--line)}+ td{padding:4px 9px;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);font-family:ui-monospace,monospace}+ td.num{color:var(--dim2)} td.nm{color:var(--text)}+ tr[data-pin]{cursor:default} tr.hot td{background:var(--hot)}+ .tp{font-size:10px;text-transform:uppercase;letter-spacing:.05em}+ .empty{color:var(--dim2);padding:30px;text-align:center;font-family:ui-monospace,monospace}`;+ }+ }+ if (!customElements.get('adom-pinout')) customElements.define('adom-pinout', AdomPinout);+})();
widgets/thumbnail-viewer.htmladded+19@@ -0,0 +1,19 @@+<!doctype html>+<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">+<title>Chip thumbnail viewer</title>+<style>html,body{margin:0;background:transparent}adom-chip-thumbnail{display:block}.msg{padding:22px;color:#8b97ac;font:13px/1.6 ui-monospace,monospace}</style></head>+<body>+<!-- Embeddable chip-thumbnail viewer. ?src=<img>&mpn=&package= · postMessage {type:'thumbnail',data} · ?theme= -->+<adom-chip-thumbnail id="w"></adom-chip-thumbnail>+<div class="msg" id="msg" hidden></div>+<script src="chip-thumbnail-widget.js"></script>+<script>+const w=document.getElementById('w'), msg=document.getElementById('msg'), P=new URLSearchParams(location.search);+if(P.get('theme')) w.setAttribute('theme', P.get('theme'));+if(P.get('src')){ w.setAttribute('src', P.get('src')); if(P.get('mpn')) w.setAttribute('mpn', P.get('mpn')); if(P.get('package')) w.setAttribute('package', P.get('package')); }+else { msg.hidden=false; msg.textContent='pass ?src=<image-url>&mpn=&package=, or postMessage {type:"thumbnail", data:{…}}'; }+addEventListener('message', e=>{ const m=e.data||{}; if(m.type==='thumbnail' && m.data){ msg.hidden=true; w.data=m.data; } });+function ph(){ try{ parent.postMessage({type:'readme-height', height:Math.max(120,Math.min(16000,document.body.scrollHeight||200))}, '*'); }catch(_){} }+addEventListener('load',ph); setTimeout(ph,400); if('ResizeObserver' in window) new ResizeObserver(ph).observe(document.body);+</script>+</body></html>