{"slug":"icons","title":"icons","summary":"Designing SVG geometry for Cerb's icon set -- the four pattern families, the fixed outer template, and the mask-image constraints that make ordinary SVG instincts wrong. Load it before drawing or editing any icon.","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-16T16:27:57.296316Z","repo":{"url":"https://github.com/jstanden/cerb","stars":84,"forks":38,"license":null,"updatedAt":"2026-10-16T15:09:59Z"},"bodyHtml":"<hr>\n<h2>name: icons\ndescription: Designing SVG geometry for Cerb's icon set -- the four pattern families, the fixed outer template, and the mask-image constraints that make ordinary SVG instincts wrong. Load it before drawing or editing any icon.</h2>\n<h1>Cerb SVG icons</h1>\n<p>Cerb icons live in an SCSS Sass map and render via CSS <code>mask-image</code>. <strong>The icon's geometry determines opacity; the button's CSS text color provides the actual visible color.</strong> Fill and stroke are both just \"opaque\" — there is no color contrast available. Every visual distinction must come from geometry: opaque shape vs transparent gap.</p>\n<h2>Output format</h2>\n<p>Each icon is a single line in a Sass map:</p>\n<pre><code>  name:           \"&lt;inner-svg-geometry&gt;\",\n</code></pre>\n<p>The geometry is <strong>strictly inner geometry</strong>. <strong>Never</strong> include the <code>&lt;svg&gt;</code> tag (start or end), <code>&lt;/svg&gt;</code> closing tags, <code>&lt;g&gt;</code> groups, or <code>xmlns</code> attributes. Just the shapes and paths. Use <strong>single quotes</strong> on SVG attributes so the whole thing fits in the double-quoted Sass string.</p>\n<h2>Outer template (applied automatically at render time)</h2>\n<pre><code>  &lt;svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'\n     fill='none' stroke='black' stroke-width='2'\n     stroke-linecap='round' stroke-linejoin='round'&gt;\n  ...your inner geometry here...\n  &lt;!-- The closing &lt;/svg&gt; is added automatically --&gt;\n&lt;/svg&gt;\n</code></pre>\n<p>Your geometry inherits:</p>\n<ul>\n<li>24×24 viewBox (everything must fit; stroke extends 1 unit past the path)</li>\n<li><code>fill='none'</code> (override per-element with <code>fill='black'</code> to fill)</li>\n<li><code>stroke='black' stroke-width='2'</code> (override per-element with <code>stroke='none'</code> to remove)</li>\n<li><code>stroke-linecap='round'</code>, <code>stroke-linejoin='round'</code> — <strong>sharp acute corners get rounded by ~1 unit</strong></li>\n</ul>\n<h2>The four pattern families</h2>\n<p>Pick a family up front and design within its constraints. Combining families in one icon usually fails because of the mask-image color limitation.</p>\n<p><strong>1. Line icon (Lucide style, default).</strong> <code>&lt;path&gt;</code>, <code>&lt;rect&gt;</code>, <code>&lt;circle&gt;</code>, <code>&lt;line&gt;</code>, <code>&lt;polyline&gt;</code>, <code>&lt;polygon&gt;</code> with no extra attributes — they inherit <code>fill='none'</code>, <code>stroke='black' stroke-width='2'</code>. Covers the vast majority of icons.</p>\n<pre><code>  mail:           \"&lt;rect x='2' y='4' width='20' height='16' rx='2'/&gt;&lt;path d='m22 7-10 6L2 7'/&gt;\",\n</code></pre>\n<p><strong>2. Filled solid icon.</strong> Add <code>fill='black'</code>. Add <code>stroke='none'</code> for crisp edges / sharp corners (small triangles, chevrons, arrow tips, pixel-precise cutouts); keep the template stroke when you want visual weight matching line icons.</p>\n<pre><code>  chevron-down:   \"&lt;path fill='black' stroke='none' d='M12 15.5l-7-7 2-2 5 5 5-5 2 2z'/&gt;\",\n</code></pre>\n<p><strong>3. Compass / ring-with-inner-element.</strong> Outlined ring + filled inner element(s).</p>\n<pre><code>  play-button:    \"&lt;circle cx='12' cy='12' r='10'/&gt;&lt;polygon fill='black' points='7.5 6 17.5 12 7.5 18'/&gt;\",\n</code></pre>\n<p><strong>4. Filled disc with a punched symbol (the <code>circle-*</code> family).</strong> Filled r=10 disc centered at (12,12), symbol cut out via <code>fill-rule='evenodd'</code> + <code>stroke='none'</code>. The symbol MUST be a single continuous closed polygon (or non-overlapping subpaths) — overlapping subpaths push the winding count odd and re-fill the cutout. When in doubt, look up a shipped <code>circle-*</code> icon and trace from its geometry.</p>\n<pre><code>  circle-plus:    \"&lt;path stroke='none' fill='black' fill-rule='evenodd' d='M2 12A10 10 0 1 0 22 12A10 10 0 1 0 2 12ZM10.5 6 13.5 6 13.5 10.5 18 10.5 18 13.5 13.5 13.5 13.5 18 10.5 18 10.5 13.5 6 13.5 6 10.5 10.5 10.5Z'/&gt;\",\n</code></pre>\n<h2>Critical mask-image constraints</h2>\n<p>Non-negotiable consequences of <code>mask-image</code> rendering:</p>\n<ol>\n<li><strong>No fill/stroke color contrast.</strong> Both are opaque. Traditional outlining isn't possible — only geometric outline tricks (negative-space halo via evenodd, or a hollow stroked shape with no fill).</li>\n<li><strong>Filled shape + stroke = bigger filled shape.</strong> The 2px stroke adds ~1 unit of opacity in every direction. Inset a polygon by 1 unit to keep its visible size when it carries the template stroke.</li>\n<li><strong>A transparent gap is required to \"outline\" anything.</strong> Shape B is only visible against filled shape A if B is outside A, a cutout in A (evenodd), or both.</li>\n<li><strong><code>stroke-linejoin='round'</code> rounds every sharp corner by ~1 unit.</strong> Use <code>stroke='none'</code> for true sharp corners.</li>\n<li><strong>ViewBox edges clip.</strong> A path at x=24 with a 2px stroke renders to x=25 and gets clipped. Pull paths in by 1 unit to preserve perimeter stroke. (The <code>circle-*</code> family can use a full r=10 disc only because it uses <code>stroke='none'</code>.)</li>\n<li><strong>Masks can't occlude — no \"in front / behind\".</strong> Overlapping shapes merge into one blob. Fanned decks, stacked cards, \"one shape behind another\" do NOT work — use distinct, spatially-separated elements.</li>\n<li><strong>Stroke eats the gap between elements.</strong> Each stroked shape extends half its <code>stroke-width</code> toward its neighbor. Budget ~4 geometric units of gap for ~2.5 visual units when spacing composites.</li>\n<li><strong>SVG <code>transform</code> renders fine in the mask</strong> (e.g. <code>transform='rotate(angle cx cy)'</code>) — but re-read #6 before overlapping.</li>\n</ol>\n<h2>Reference docs (mounted at <code>@cerb-docs</code>, read-only)</h2>\n<p>You have the full Cerb documentation mounted. Read these instead of guessing — especially before claiming an icon does or doesn't exist.</p>\n<table>\n<thead>\n<tr>\n<th>Path</th>\n<th>What it gives you</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>@cerb-docs/references/docs/developers/icons.md</code></td>\n<td><strong>The canonical icon list.</strong> Every registered icon name in the shipped set, plus why an icon is a single-color mask that inherits <code>currentColor</code>. Check here first for naming conventions and prior art (<code>circle-*</code>, <code>logo-*</code>, <code>chart-*</code>, <code>window-*</code>, <code>move-*</code> families).</td>\n</tr>\n<tr>\n<td><code>@cerb-docs/references/docs/setup/developers/icon-builder.md</code></td>\n<td>The Icon Builder: it holds <strong>inner geometry only</strong>, the wrapper <code>&lt;svg&gt;</code> is fixed by the icon pipeline, and output is two source lines (the <code>$icons</code> entry in <code>cerb-icons.scss</code> + the name entry in the UI service). Nothing is saved server-side.</td>\n</tr>\n<tr>\n<td><code>@cerb-docs/references/docs/data-queries/ui/icons.md</code></td>\n<td><code>type:ui.icons</code> data query — filterable/pageable list of icon names, the same source behind the icon-listing tool and the in-app pickers.</td>\n</tr>\n<tr>\n<td><code>@cerb-docs/references/docs/toolbars.md</code></td>\n<td>How <code>icon:</code> is consumed on toolbar buttons and menu links (icon in addition to, or instead of, a label).</td>\n</tr>\n<tr>\n<td><code>@cerb-docs/references/docs/sheets.md</code></td>\n<td>The <code>icon</code> column type and the <code>icon:</code>/<code>icon_at:</code> parameters on card, link, interaction, and button columns — the other main place icons render.</td>\n</tr>\n</tbody>\n</table>\n<p>Practical notes that follow from those pages:</p>\n<ul>\n<li><strong>An icon name must be registered to be accepted.</strong> Icon pickers, toolbars, and sheets reject unknown names (they silently render nothing), so a new glyph isn't usable until both source lines are copied out and the CSS is rebuilt.</li>\n<li><strong>Names are the API.</strong> Match the existing naming conventions in the icon reference — kebab-case, family prefix first (<code>circle-plus</code>, not <code>plus-circle</code>; <code>chart-bar-stacked</code>, <code>window-left</code>).</li>\n<li><strong>Check for an existing icon before drawing one.</strong> Look it up in the shipped set or the icon reference; adapting a shipped glyph's geometry keeps the house style better than inventing.</li>\n<li>Icons are sized and colored by their context (buttons, panels, sheet cells, at a range of sizes), so geometry must read at small sizes with no color contrast available.</li>\n</ul>\n<h2>Draft, look, refine -- do not try to one-shot it</h2>\n<p><code>cerb_set_geometry</code> pushes a revision and the preview updates immediately, so you can SEE the icon. That makes a rough first pass cheap and a long analytical build expensive. Get something on screen in your first turn or two, then adjust.</p>\n<ul>\n<li><strong>Ship the draft early.</strong> A plain first version -- right family, right proportions, nothing refined -- teaches you more in one turn than another round of reasoning does. The user is watching the preview and will often correct you before you would have caught it yourself.</li>\n<li><strong>Refine in small passes.</strong> One change per revision, or a couple of related ones. Every <code>cerb_set_geometry</code> is undoable, so a wrong step costs a revision rather than the work.</li>\n<li><strong>Reason about geometry when the preview disagrees with you.</strong> Winding at the center of an evenodd cutout, stroke growth against the viewBox edge, the gap eaten between two stroked shapes -- these are diagnoses for a symptom you can see, not a checklist to run before drawing.</li>\n<li><strong>Don't precompute what you can just look at.</strong> Curve math, traced coordinates, a scratch file of alternatives: reach for those when a shape is genuinely fiddly, not by default.</li>\n<li><strong>Say where you are.</strong> \"Here is a first pass -- I will tighten the stem next\" sets the expectation that this is a draft. Several silent minutes reads as being stuck, and a perfect icon delivered late is worth less than a good one delivered in three quick rounds.</li>\n</ul>\n<h2>Working approach</h2>\n<ol>\n<li><strong>Start from Lucide.</strong> Most icons map to an existing Lucide design — adapt rather than invent. Lucide path data drops in cleanly after converting double quotes to single quotes.</li>\n<li><strong>Pick a pattern family up front</strong> and design within its constraints.</li>\n<li><strong>If an evenodd cutout fills in solid, check the winding at its center</strong> — for a disc + symbol, the symbol's center must be winding 2 (transparent). That is the diagnosis for that symptom, not a step to run before every draft.</li>\n<li><strong>Take multi-icon requests one at a time.</strong> An icon editor holds a single icon's geometry, so work through a set in sequence — settle one glyph, note its shared parameters (stroke weight, corner radius, optical size), and carry them into the next so the family stays consistent.</li>\n<li><strong>Iterate on dimensions, not approach.</strong> \"Fatter / thinner / shorter / deeper\" → adjust existing parameters. \"Different style entirely\" → switch pattern family.</li>\n<li><strong>Round to 2 decimals max</strong> unless precision matters (e.g. cutout sagitta math).</li>\n<li><strong>Use your scratch area when a shape genuinely needs it</strong> — traced path data, winding and sagitta math, intermediate geometry held across steps. Prefer looking at the preview over computing what it would have looked like.</li>\n</ol>\n","files":[{"path":"SKILL.md","sizeBytes":9929,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-16T16:28:14.655102Z","sha256":"5D7FBFB2473ED23B5872EB1E1F566B7F8360B3BB1ACB71A812F1E10852BB52D0","sizeBytes":4596},"review":null,"source":{"repositoryUrl":"https://github.com/jstanden/cerb","path":"features/cerberusweb.core/assets/agent_filesystems/cerb-agents/skills/icons","license":null,"commit":"cf28b62ee0f67ff993645931568a4e4bbad074f0","subtreeSha":"F0D867F303236DE7382DF833F9DFEAD5740F744CECCD6527A629CC24692AA94E","lastSyncedAt":"2026-10-16T16:27:56.786317Z"},"reviewedAt":"2026-10-16T16:28:23.807005Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/jstanden/cerb/tree/12.0.x/features/cerberusweb.core/assets/agent_filesystems/cerb-agents/skills/icons"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jstanden-cerb@llmmart"},{"target":"git","command":"git clone https://github.com/jstanden/cerb.git"}]}