Skip to content

Feature: Add ankh-icon component with Material Symbols font rendering#13

Merged
imagineux merged 9 commits intomainfrom
CU-86af6pbbr_Implement-ankh-icon-component_Matthew-Van-Dusen
Feb 18, 2026
Merged

Feature: Add ankh-icon component with Material Symbols font rendering#13
imagineux merged 9 commits intomainfrom
CU-86af6pbbr_Implement-ankh-icon-component_Matthew-Van-Dusen

Conversation

@imagineux
Copy link
Contributor

Summary

Add ankh-icon component that renders Material Symbols icons via ligature resolution on the variable icon font

Changes

Notes

@Brandon-Anubis
Copy link

Task linked: CU-86af6pbbr Implement ankh-icon component

@qodo-code-review
Copy link

Review Summary by Qodo

Add ankh-icon component with Material Symbols font rendering

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add ankh-icon component rendering Material Symbols via font ligatures
• Support size variants (sm/md/lg/xl) with optical size axis tracking
• Implement filled variant via FILL axis of variable font
• Provide accessibility features with optional label prop
• Include comprehensive unit and a11y tests with full coverage
Diagram
flowchart LR
  A["Material Symbols Font"] -->|"ligature resolution"| B["ankh-icon Component"]
  B -->|"size prop"| C["Size Variants<br/>sm/md/lg/xl"]
  B -->|"filled prop"| D["Fill Axis<br/>outlined/filled"]
  B -->|"label prop"| E["Accessibility<br/>decorative/meaningful"]
  C -->|"optical sizing"| F["Visual Clarity"]
  D -->|"FILL axis"| F
  E -->|"role/aria-label"| F
Loading

Grey Divider

File Changes

1. src/components/ankh-icon/ankh-icon.tsx ✨ Enhancement +59/-0

Core icon component with props and rendering

src/components/ankh-icon/ankh-icon.tsx


2. src/components/ankh-icon/ankh-icon.css ✨ Enhancement +94/-0

Styles with font axes and size token mapping

src/components/ankh-icon/ankh-icon.css


3. src/components/ankh-icon/ankh-icon.spec.ts 🧪 Tests +202/-0

Comprehensive unit tests for all props and states

src/components/ankh-icon/ankh-icon.spec.ts


View more (8)
4. src/components/ankh-icon/ankh-icon.a11y.spec.ts 🧪 Tests +76/-0

Accessibility tests with axe integration

src/components/ankh-icon/ankh-icon.a11y.spec.ts


5. src/components/ankh-icon/readme.md 📝 Documentation +20/-0

Auto-generated component documentation

src/components/ankh-icon/readme.md


6. src/components.d.ts ✨ Enhancement +51/-0

TypeScript declarations for ankh-icon component

src/components.d.ts


7. docs/adr/002-icon-rendering-strategy.md 📝 Documentation +89/-0

Architecture decision for font-based icon strategy

docs/adr/002-icon-rendering-strategy.md


8. docs/adr/README.md 📝 Documentation +1/-0

Update ADR index with new icon strategy decision

docs/adr/README.md


9. README.md 📝 Documentation +32/-1

Add icon component usage and versioning guidance

README.md


10. .github/PULL_REQUEST_TEMPLATE.md ⚙️ Configuration changes +9/-0

Add SemVer impact checklist to PR template

.github/PULL_REQUEST_TEMPLATE.md


11. package.json Dependencies +3/-1

Add jsdom and vitest-axe dev dependencies

package.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 18, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Axe tests vs happy-dom 🐞 Bug ⛯ Reliability
Description
New a11y tests use vitest-axe/axe, but the repo-wide Vitest environment remains happy-dom; if axe
requires jsdom APIs, CI can become flaky or fail. The newly-added jsdom devDependency appears
unused unless the environment is switched/configured for these tests.
Code

src/components/ankh-icon/ankh-icon.a11y.spec.ts[R1-4]

+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import * as matchers from 'vitest-axe/matchers';
+import { axe } from 'vitest-axe';
+import type { AxeMatchers } from 'vitest-axe';
Evidence
The new a11y test file imports and runs axe(), while Vitest is configured globally to run tests in
happy-dom. The PR also adds jsdom and vitest-axe, suggesting an intended jsdom-based a11y
setup that is not yet wired in.

src/components/ankh-icon/ankh-icon.a11y.spec.ts[1-4]
src/components/ankh-icon/ankh-icon.a11y.spec.ts[30-34]
vitest.config.ts[4-10]
package.json[56-76]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `vitest-axe` a11y tests run under the repo-wide `happy-dom` environment. If `axe` expects jsdom-specific DOM APIs, these tests can fail or be flaky in CI. The PR adds `jsdom` but does not configure Vitest to use it.
### Issue Context
- Vitest is configured with `environment: &amp;amp;amp;amp;amp;amp;amp;#x27;happy-dom&amp;amp;amp;amp;amp;amp;amp;#x27;`.
- New a11y tests import and execute `axe()`.
- `jsdom` was added to devDependencies, suggesting intent to use it.
### Fix Focus Areas
- vitest.config.ts[4-10]
- src/components/ankh-icon/ankh-icon.a11y.spec.ts[1-4]
- package.json[56-76]
### Suggested implementation notes
- Option A (recommended): in `vitest.config.ts`, add `environmentMatchGlobs` to run `**/*.a11y.spec.ts` in `jsdom`.
- Option B: add `/** @vitest-environment jsdom */` at the top of `ankh-icon.a11y.spec.ts`.
- If staying on happy-dom, document/verify compatibility and remove `jsdom` if unused.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

✅ 2. ankh-icon hardcoded size fallbacks 📘 Rule violation ✓ Correctness
Description
The new ankh-icon styling introduces hardcoded px fallback values for sizing even though shared
--icon-size-* design tokens are referenced. This can undermine token-driven theming/consistency if
the fallback values diverge from the design token source of truth.
Code

src/components/ankh-icon/ankh-icon.css[R23-73]

+  font-size: var(--icon-size-md, 24px);
+
+  /* Layout */
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  line-height: 1;
+  letter-spacing: normal;
+  text-transform: none;
+  white-space: nowrap;
+  word-wrap: normal;
+  direction: ltr;
+  flex-shrink: 0;
+
+  /* Color: inherit from parent, never hardcoded */
+  color: currentColor;
+
+  /* Interaction */
+  user-select: none;
+
+  /* Font rendering */
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  text-rendering: optimizeLegibility;
+  font-feature-settings: 'liga';
+}
+
+/* ========================================
+   Size variants
+   Optical size tracks rendered size per M3 guidance.
+   opsz axis range: 20–48 (continuous).
+   ======================================== */
+
+.ankh-icon--sm {
+  font-size: var(--icon-size-sm, 18px);
+  --_opsz: 20; /* Clamped from 18 to axis minimum 20 */
+}
+
+.ankh-icon--md {
+  font-size: var(--icon-size-md, 24px);
+  --_opsz: 24;
+}
+
+.ankh-icon--lg {
+  font-size: var(--icon-size-lg, 36px);
+  --_opsz: 40;
+}
+
+.ankh-icon--xl {
+  font-size: var(--icon-size-xl, 48px);
+  --_opsz: 48;
Evidence
Compliance requires avoiding hardcoded styling values when a shared design token exists. The new CSS
uses --icon-size-sm/md/lg/xl tokens but also adds hardcoded fallback pixel values (18px, 24px,
36px, 48px) in the same declarations.

src/components/ankh-icon/ankh-icon.css[23-24]
src/components/ankh-icon/ankh-icon.css[56-73]
Best Practice: Organization repository standards

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ankh-icon` size styles include hardcoded pixel fallbacks (`18px`, `24px`, `36px`, `48px`) even though `--icon-size-*` design tokens are used.
## Issue Context
Design system compliance requires using shared tokens for sizing when tokens exist, to preserve themeability and consistency.
## Fix Focus Areas
- src/components/ankh-icon/ankh-icon.css[23-24]
- src/components/ankh-icon/ankh-icon.css[56-73]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unclamped size values 🐞 Bug ✓ Correctness
Description
size is rendered directly into a CSS class name with no runtime validation. In untyped usage
(plain HTML/JS or dynamic attribute setting), invalid values will yield an unmatched class (e.g.,
ankh-icon--large) and incorrect styling/opsz behavior.
Code

src/components/ankh-icon/ankh-icon.tsx[R48-52]

+        <span
+          class={cn('ankh-icon', `ankh-icon--${this.size}`, this.filled && 'ankh-icon--filled')}
+          role={isDecorative ? undefined : 'img'}
+          aria-label={this.label || undefined}
+          aria-hidden={isDecorative ? 'true' : undefined}
Evidence
The component always emits ankh-icon--${this.size}, but the stylesheet only defines sm/md/lg/xl
variants. TypeScript helps typed consumers, but the runtime still accepts arbitrary attribute
values.

src/components/ankh-icon/ankh-icon.tsx[47-52]
src/components/ankh-icon/ankh-icon.css[56-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`size` is used to build a CSS class string without runtime validation. Invalid values (from plain HTML/JS usage or dynamic `setAttribute`) produce an unmatched class and break sizing/opsz styling.
### Issue Context
CSS only defines `.ankh-icon--sm|md|lg|xl`.
### Fix Focus Areas
- src/components/ankh-icon/ankh-icon.tsx[43-55]
- src/components/ankh-icon/ankh-icon.css[56-74]
### Suggested implementation notes
- Introduce a local normalized value:
- `const size = ([&amp;amp;amp;amp;amp;amp;amp;#x27;sm&amp;amp;amp;amp;amp;amp;amp;#x27;,&amp;amp;amp;amp;amp;amp;amp;#x27;md&amp;amp;amp;amp;amp;amp;amp;#x27;,&amp;amp;amp;amp;amp;amp;amp;#x27;lg&amp;amp;amp;amp;amp;amp;amp;#x27;,&amp;amp;amp;amp;amp;amp;amp;#x27;xl&amp;amp;amp;amp;amp;amp;amp;#x27;] as const).includes(this.size) ? this.size : &amp;amp;amp;amp;amp;amp;amp;#x27;md&amp;amp;amp;amp;amp;amp;amp;#x27;;`
- Use `size` when computing the class name.
- Optionally `console.warn` (guarded) when an invalid value is received.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 4. Whitespace label semantics 🐞 Bug ✓ Correctness
Description
label is treated as meaningful whenever it is truthy, but whitespace-only labels (e.g., `label="
") become role="img" with a useless/blank aria-label`. This can silently create inaccessible
icons in real usage (bindings often produce empty/whitespace strings).
Code

src/components/ankh-icon/ankh-icon.tsx[R43-53]

+  render() {
+    const isDecorative = !this.label;
+
+    return (
+      <Host class="ankh-icon-host">
+        <span
+          class={cn('ankh-icon', `ankh-icon--${this.size}`, this.filled && 'ankh-icon--filled')}
+          role={isDecorative ? undefined : 'img'}
+          aria-label={this.label || undefined}
+          aria-hidden={isDecorative ? 'true' : undefined}
+        >
Evidence
Meaningful-vs-decorative is decided by !this.label (no trimming/normalization). aria-label is
then set directly from label, so whitespace strings are passed through.

src/components/ankh-icon/ankh-icon.tsx[43-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Whitespace-only labels are currently treated as meaningful, causing `role=&amp;amp;amp;amp;amp;amp;amp;quot;img&amp;amp;amp;amp;amp;amp;amp;quot;` with an effectively empty `aria-label`, which is an accessibility footgun.
### Issue Context
`isDecorative` is computed via `!this.label` and `aria-label` is set directly from `label`.
### Fix Focus Areas
- src/components/ankh-icon/ankh-icon.tsx[43-55]
### Suggested implementation notes
- Compute `const normalizedLabel = this.label?.trim();`
- Use `const isDecorative = !normalizedLabel;`
- Set `aria-label={normalizedLabel || undefined}`
- Optional: warn (dev-only) if `label` is provided but trims to empty.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@imagineux imagineux merged commit 1376ae2 into main Feb 18, 2026
4 checks passed
@imagineux imagineux deleted the CU-86af6pbbr_Implement-ankh-icon-component_Matthew-Van-Dusen branch February 18, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants