# AB#1372704

## Executive Summary

Pictograms — the larger, illustrative graphics designers use to tell a story — previously had no single home. They were scattered across Figma and various site pages, so teams couldn't reliably find the current, approved version or reuse it with confidence. This work adds **pictograms as a new collection inside the existing Auro icon library**, sitting alongside the icons and tails it already provides. Pictograms now go through the same automated optimization, publishing, and versioning pipeline, come with documentation that explains how they differ from icons and tails, and ship with automated tests that confirm they are correctly built and published. The first approved pictogram (`elbow-room-on-light`, delivered by Design on the ticket) is included as the seed of the collection. Follow-up passes in this workstream also closed two subtle bugs so the library keeps its promises: pictograms are shown in their own brand colors and never re-tinted, and they scale fluidly to fit their container instead of overflowing it. A subsequent code-review pass hardened the collection further — making each pictogram's internal graphics self-contained so different pictograms can safely share a page — and tidied the generated markup.

Ticket: [AB#1372704](https://itsals.visualstudio.com/E_Retain_Content/_workitems/edit/1372704)

## The Problem

There was no shared, versioned source of truth for pictograms. Because they lived in Figma and on assorted pages, different teams could end up using different or outdated versions of the same artwork, and there was no easy way to browse the approved set or pull it into a product. The icon library already solved this exact problem for icons and tails — but pictograms had no equivalent home, so they missed out on the same optimization, versioning, and documentation.

Two quieter problems surfaced during the work. First, the pipeline would silently re-color pictograms: they are supposed to keep their own fixed brand palette, but the build was injecting a "use the surrounding text color" rule onto every pictogram. Second, the pipeline forced pictograms to a minimum width equal to their container, so a pictogram dropped into a tight space would refuse to shrink and could spill outside its layout. Both were harmless for the first asset by luck, but each contradicted how pictograms are meant to behave and would have bitten future assets and layouts. A later review surfaced a third, still-latent hazard: because pictograms are meant to be dropped straight into a page's markup, the internal identifiers inside each graphic could clash once more than one pictogram appears together — a class of problem that is invisible with a single asset but grows more likely as the set expands.

## Root Cause

This is largely an additive capability rather than a defect, but there are four root causes worth separating.

**The structural gap.** The library's build pipeline only knew about three collections. In [scripts/generate.js](../../scripts/generate.js), the generator explicitly loaded and ran three manifests — `icons.json`, `restrictedIcons.json`, and `logoIcons.json` — and had hardcoded output-routing branches for the `restricted` and `logos` categories. There was no manifest, no source folder, no output route, no demo section, and no test coverage for a "pictograms" category, so there was no path for pictogram artwork to enter the published output.

**The tinting bug.** [scripts/generate.js](../../scripts/generate.js) built the root `<svg>` inline style with an unconditional `fill: ${icon.color}` declaration, and the pictograms manifest ([src/data/pictograms.json](../../src/data/pictograms.json)) carried `"color": "currentColor"` (copied from the icon/tail collections). The result was `fill:currentColor` on the root `<svg>` of every pictogram — a re-tint hook that contradicts [docs/pictograms.md](../../docs/pictograms.md)'s explicit promise that pictograms are **not** recolored via `currentColor`. It was inert for `elbow-room-on-light` because every `<path>` has an explicit brand fill (`#0074CA`, `#DBDBDB`, …), but it was a latent bug for any future pictogram — or `-on-dark` variant — with a path that inherited the fill.

**The width-mapping bug.** [scripts/generate.js](../../scripts/generate.js) mapped every collection's `icon.width` onto a CSS **`min-width`** declaration (`const width = icon.width ? \`min-width: ${icon.width};\` : ''`). That is correct for UI glyphs, which use a fixed pixel size and benefit from a floor that stops them collapsing. But the pictograms manifest declares a **fluid** `"width": "100%"`, so the pictogram emitted `min-width: 100%` with no plain `width`. `min-width: 100%` forbids the illustration from ever shrinking below its container's full width, so a consumer placing it in a constrained flex or grid child would see it overflow the parent — the opposite of the fluid behavior a `100%` width implies. Latent because it only manifests in constrained consumer layouts.

**The id-collision hazard (latent, surfaced in code review).** Pictograms are documented for inline consumption (`element.innerHTML = icon.svg`), so a pictogram's internal SVG ids share a document with everything else on the page. The seed asset carries a Figma-exported clip-path id (`clip0_1429_2007`) referenced by `<g clip-path="url(#clip0_1429_2007)">`, and the SVGO config in [scripts/generate.js](../../scripts/generate.js) deliberately sets `cleanupIds: false` (so it never rewrites the internal id references the full-color logo/restricted marks depend on). With ids passed through verbatim, two *different* pictograms whose Figma exports happened to share an auto-incremented id would collide when placed on the same page: the browser resolves every `url(#…)` to the first matching definition, so one pictogram's clip region would silently clip another. Harmless with today's single asset — a latent defect as the collection grows.

## The Fix

The change wires a fourth collection into the existing pattern rather than inventing new infrastructure, removes both latent behavioral bugs, and hardens the collection against the id-collision class:

- **Source asset** — Design's delivered file was added at [src/icons/pictograms/elbow-room-on-light.svg](../../src/icons/pictograms/elbow-room-on-light.svg) (fetched from the ticket attachment).
- **Manifest** — [src/data/pictograms.json](../../src/data/pictograms.json), modeled on `logoIcons.json`: `category: "pictograms"`, `path: "/icons/pictograms"`, `type: "pictogram"`, with a per-item `viewBox` (`0 0 100 50`) because pictograms vary in aspect ratio. It deliberately does **not** declare a `color`, so no tint is applied.
- **Pipeline** — [scripts/generate.js](../../scripts/generate.js) now requires and runs the new manifest, routes its output to `dist/pictograms/` via a `category === 'pictograms'` branch alongside the existing logos/restricted branches, and includes `type === 'pictogram'` in the condition that emits a modern `.mjs` ESM entry.
- **Coloring fix** — the root-`<svg>` style now only appends `fill: …` when the collection actually opts in with a `color`: `const fillStyle = icon.color ? \`fill: ${icon.color}\` : ''`. Because the pictograms manifest declares no `color`, pictograms render with no `fill` hook. Icons, tails/logos, and restricted marks still declare `color: "currentColor"` and are unchanged.
- **Fluid-width fix** — the width declaration now chooses its property by collection: `const widthProp = icon.category === 'pictograms' ? 'width' : 'min-width'`. Pictograms therefore emit `style="width:100%;height:auto"` and can shrink to fit constrained flex/grid parents, while every other collection keeps `min-width` byte-for-byte.
- **Id namespacing** — a `scopeSvgIds()` pass runs for the pictograms category only, prefixing every id (and its `url(#…)` / `href` references) with the icon name — `clip0_1429_2007` → `elbow-room-on-light__clip0_1429_2007` — before `<title>`/`<desc>` injection and optimization. Each pictogram's internal references are now globally unique, so different pictograms can't collide on a shared page. Scoped to `category === 'pictograms'` so the other three collections' emitted ids are unchanged.
- **Style-attribute tidy** — the root `<svg>` style is assembled with `\`style="${[width, height, fillStyle].filter(Boolean).join(' ')}"\`` so an empty `fillStyle` (pictograms) no longer leaves a trailing double space in the attribute.
- **Demo gallery** — [scripts/buildDemo.js](../../scripts/buildDemo.js) gained a `buildPictograms()` helper and a "Pictograms" section, cloned from the existing restricted block.
- **Tests** — [tests/pictograms.test.js](../../tests/pictograms.test.js) iterates the manifest and asserts each published pictogram object exposes `title`/`name`/`desc`/`style`/`svg`, that its `svg` **preserves at least one explicit brand hex fill** (`fill="#…"`), that its `svg` is **not** recolored (no `fill:currentColor` anywhere), that it uses **fluid `width:100%` and no `min-width`**, that every id and `url(#…)` reference is **namespaced to the icon name**, and that the documented **`.mjs` entry exists and parses** to the same icon object as the `.js` entry.
- **Docs** — [docs/pictograms.md](../../docs/pictograms.md) distinguishes pictograms from icons and tails, defines the animated-pictogram policy, and notes that build-time id namespacing prevents cross-pictogram collisions but that rendering the **same** pictogram inline multiple times still needs consumer-side id isolation; [README.md](../../README.md) gained a "Collections" table pointing to it.

## Why This Works

The library already had a proven model for full-color, non-`currentColor` collections (tails/logos and restricted). Pictograms are the same shape of problem — colored, illustrative, per-item `viewBox` — so following that path meant the existing SVGO optimization, the `.js`/`.mjs`/`_es6.js`/`.svg` emission, and the merged `index` all worked with minimal additions.

Brand colors are now preserved by **two** independent mechanisms, which is what makes the fix correct rather than incidental:

1. **Fills survive optimization.** The generator strips hardcoded `fill="#..."` values *only* when `path === '/icons'`. Because pictograms use `path: "/icons/pictograms"`, their explicit fills are left intact through SVGO.
2. **No re-tint is injected.** With the manifest carrying no `color`, the root `<svg>` gets no `fill` declaration at all, so there is nothing to override a shape that would otherwise use its own fill. Previously mechanism 1 held for the seed asset only by luck (every path was explicit); the `fill:currentColor` hook would have tinted the first path that wasn't. Removing the hook restores the documented invariant for *all* future pictograms.

The width fix is scoped the same way. A fluid `width: 100%` means "be as wide as my container allows, but shrink when it doesn't" — which is exactly what plain `width` expresses and `min-width` forbids. Keying the property on `category === 'pictograms'` fixes the fluid collection while leaving the fixed-pixel UI glyphs on `min-width`, where a shrink-floor is the right behavior. The invariant is now encoded as a test (fluid `width`, no `min-width`), so a future refactor of the style builder can't silently reintroduce the overflow.

The id-namespacing is scoped identically — keyed on `category === 'pictograms'` — so icons, tails/logos, and restricted marks keep their original ids and their output stays byte-for-byte identical. Prefixing each id with the icon name makes a pictogram self-contained, which eliminates collisions between *different* pictograms on the same page. It intentionally does **not** try to solve the *same* pictogram rendered inline more than once: those instances necessarily share the icon name, so identical ids are unavoidable at build time. That residual case is a consumer responsibility (isolate each instance in a shadow root, or rewrite ids per instance) and is called out in the docs rather than papered over. The namespacing runs *before* `<title>`/`<desc>` injection so the accessibility ids (`<name>__title` / `<name>__desc`) are added exactly once and never double-prefixed, and the whole invariant is locked in by a test. Keeping `cleanupIds: false` is still required — the color collections' internal references depend on it — so namespacing (rather than stripping) is the fix that preserves those references while making pictogram ids unique.

Both the coloring, width, and id conditionals are scoped so the other three collections are untouched — they still declare `color: "currentColor"`, still emit `fill:currentColor`, still use `min-width`, and still carry their original ids, which is correct for monochrome/tinted, fixed-size UI icons.

Pictograms remain intentionally excluded from `svglint`: the `.svglintrc.js` rules forbid a `fill` attribute on `<path>` elements, which is why full-color tails/logos and restricted marks are already absent from the `svglint` script. Adding colored pictograms to that lint step would fail the build, so — consistent with the other color collections — they are validated by the build/output tests instead.

Verified end to end: a clean `node scripts/generate.js` emits `dist/pictograms/elbow-room-on-light.{svg,js,mjs,_es6.js}` with the root `<svg>` now reading `style="width:100%;height:auto"` (no `fill:currentColor`, no `min-width`, no trailing whitespace), its internal clip-path id namespaced to the icon (`elbow-room-on-light__clip0_1429_2007`), `<title>`/`<desc>` injected, `role="img"`, correct `viewBox`, and all brand fills preserved; the merged `dist/index_es5.js` includes the `elbow-room-on-light` key with `type: pictogram`; a spot-check confirms regular icons (e.g. `error-filled`) still carry `fill:currentColor` and `min-width`; and `npm test` passes **3,691 tests** — including the five pictogram-specific assertions (brand-fill preserved, no `currentColor` re-tint, fluid `width`, namespaced ids, parseable `.mjs`) — with `jsonlint` and `svglint` green.

## Outcome

Pictograms now have a real home in the Auro icon library. Teams can pull the approved artwork from a single, versioned source that is optimized for the web and published the same way icons and tails already are, and there's documentation explaining when to reach for a pictogram versus an icon or a tail. Crucially, pictograms now display in their intended brand colors and are guaranteed not to be re-tinted, and they scale fluidly to fit whatever space a product gives them instead of overflowing it — so the collection behaves exactly as documented, including for the light/dark background variants planned next. As the collection grows, different pictograms can also be placed on the same page without their internal graphics interfering with one another. The collection ships with one approved pictogram today and is set up so additional pictograms — and light/dark variants — can be added by dropping a file in and adding a manifest entry, with no further engineering work.

## Ticket Completeness

**5 of 8 acceptance-checklist items resolved; 2 partial, 1 not resolved (a release-process gate).**

**Resolved**

- **Design delivers finalized source file(s)** — Design attached `elbow-room-on-light.svg` to the ticket; it's now the collection's source asset.
- **New pictograms collection alongside icons and tails** — added via the manifest + generator wiring, publishing to `dist/pictograms/`.
- **Optimized for web and published/versioned like icons and tails** — runs through the same SVGO optimization and multi-format emission, published via the existing npm/versioning pipeline, with brand colors correctly preserved, no re-tinting, fluid sizing, and collision-safe internal ids.
- **Animated-pictogram handling defined and documented** — documented as static-SVG-only, with animated/GIF sources explicitly deferred and a `prefers-reduced-motion` guardrail noted for any future support.
- **Automated build/output tests confirm optimization and publishing, pass in CI** — `tests/pictograms.test.js` added (including brand-fill-preservation, no-`currentColor`, fluid-`width`, namespaced-id, and parseable-`.mjs` assertions); the full suite (3,691 tests) passes under the existing jest/CI step.

**Not Resolved / Partial**

- **Consolidate the approved set from Figma and scattered pages** — *Partial.* Only the single pictogram Design delivered is included; the full canonical set still needs to be agreed with Design and added.
- **Documentation browsable on the Auro documentation site** — *Partial.* In-repo documentation was added, but publishing/rendering it on the separate Auro documentation site (including visual-regression coverage there) is outside this repository and not yet done.
- **Design reviews/approves the set and its doc-site presentation before release** — *Not resolved.* This is a release-process gate requiring Design sign-off; it is not something the code change can satisfy and there is no evidence of approval yet.

## Learnings

- The icon library already generalizes cleanly to new collections: a manifest + a source folder + a small routing branch in `generate.js` is the whole recipe. Reusing the tails/logos path (rather than the monochrome-icons path) was the right call for full-color artwork.
- **Copying `commonProperties` between collections carries hidden behavior.** The pictograms manifest inherited `color: "currentColor"` from the icon/tail template, which quietly re-enabled the very re-tinting the collection is documented not to do. When a new collection is "like" an existing one, audit each inherited property against that collection's actual contract, not just the shape of the file.
- **Shared style-builder assumptions don't transfer to a fluid collection.** The generator's `icon.width → min-width` mapping was written for fixed-pixel glyphs; reused unchanged for a `width: 100%` pictogram it produced an overflow floor. A value that is fluid rather than fixed usually wants a different CSS property — check the *semantics* of an inherited mapping, not just that it compiles.
- **Inline SVG consumption makes ids global state.** A collection consumed via `element.innerHTML` shares a document with every other instance, so any id an asset carries — especially generic, tool-generated ones like Figma's `clip0_1429_2007` — is a latent collision, the more so because `cleanupIds: false` is required to protect the color collections' internal references. Namespace asset ids at build time, and be explicit in the docs about the same-asset-twice case the build can't solve.
- **A latent bug can hide behind lucky data.** `fill:currentColor` was inert only because every path in the seed asset had an explicit fill; the `min-width` overflow only bites in a constrained layout; the id collision only bites once a second pictogram shares a page. The first pictogram (or `-on-dark` variant, tight container, or shared page) that broke the luck would have regressed. Encoding each invariant as a test (no `currentColor`, brand fill present, fluid `width`, namespaced ids, parseable `.mjs`) is what makes it safe to add assets later.
- **Test the documented consumption path.** `docs/pictograms.md` advertises the `.mjs` file as the preferred import, but nothing asserted it existed or parsed until a test was added for it. If docs promise an entry point, a test should guard it.
- `typeof === 'string'` shape checks prove a field *exists*, not that it's *correct*. The defining properties of a pictogram — brand fills that survive the build, fluid sizing, collision-safe ids, a working modern entry point — needed their own assertions.
- The `fill`-stripping behavior is keyed on `path === '/icons'`, and `svglint` is deliberately scoped to monochrome collections. Both are easy to trip over — any new *color* collection must use a non-`/icons` path, declare no `color`, and stay out of the `svglint` script, relying on build/output tests instead.
- Not every acceptance-criterion is an engineering task. Several items here (consolidating the full Figma set, doc-site publishing, Design sign-off) live outside this repo or are process gates — worth separating "code complete" from "ticket complete" when reporting status.
- The collection is now extensible with zero code changes: additional pictograms and `-on-dark` variants only need a new SVG plus a manifest entry.

## Iterations That Didn't Work

- **First test used `getDistFilename(icon)` directly on the raw manifest entry** (copied from `tests/icons.test.js`) and failed with `Cannot find module '../dist/elbow-room-on-light.js'`. Unlike `icons.json`, this collection keeps `category` in `commonProperties` (mirroring `logoIcons.json`), so the per-item object had no `category` and the resolved path dropped the `pictograms/` folder. The fix was to merge `commonProperties` into each item before resolving the filename — the same merge the generator performs — so the test targets `dist/pictograms/<name>.js`.
- **Leaving `color: "currentColor"` in the manifest and trying to special-case pictograms downstream** was considered and rejected. The cleaner fix was to make the `fill` injection conditional in `generate.js` and simply not declare a `color` for the collection, so the behavior falls out of the data rather than a category-specific branch — and the three existing collections stay byte-for-byte identical.
- **Globally switching the width mapping from `min-width` to `width`** was rejected for the same reason: it would have changed the emitted style for icons, tails/logos, and restricted marks, where `min-width` is the intended behavior. Scoping the property choice to `category === 'pictograms'` fixes the fluid collection while leaving the other three unchanged.
- **Globally namespacing every collection's ids** (rather than only pictograms) was rejected on the same byte-for-byte grounds: it would rewrite the emitted SVGs for icons, tails/logos, and restricted marks. Scoping `scopeSvgIds()` to `category === 'pictograms'` fixes the collision class for the new collection while leaving the existing three untouched.
- **Stripping the ids entirely / enabling `cleanupIds`** was not viable: the color collections (logos/restricted) rely on internal id references that `cleanupIds` would rewrite or remove, which is exactly why the SVGO config sets `cleanupIds: false`. Namespacing preserves those references while making pictogram ids unique.
