Skip to content

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

Closed
imagineux wants to merge 3 commits intomainfrom
CU-86af6pbbr_Implement-ankh-icon-component_Matthew-Van-Dusen
Closed

Feature: Add ankh-icon component with Material Symbols font rendering#10
imagineux wants to merge 3 commits intomainfrom
CU-86af6pbbr_Implement-ankh-icon-component_Matthew-Van-Dusen

Conversation

@imagineux
Copy link
Contributor

@imagineux imagineux commented Feb 9, 2026

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
• Implement composable CSS custom properties for size and fill axes
• Include 22 unit tests and 7 axe-core accessibility compliance tests
• Document icon rendering strategy in ADR-002 with design rationale
• Add jsdom and vitest-axe dev dependencies for testing
Diagram
flowchart LR
  A["ankh-icon Component"] --> B["Props: name, size, filled, label"]
  B --> C["Light DOM Rendering"]
  C --> D["Material Symbols Font"]
  D --> E["Ligature Resolution"]
  A --> F["CSS Styling"]
  F --> G["Private Custom Properties"]
  G --> H["Composable Size & Fill"]
  A --> I["Accessibility"]
  I --> J["Decorative or Meaningful Icons"]
  J --> K["WCAG Compliance"]
Loading

Grey Divider

File Changes

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

Core icon component implementation

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


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

Styling with composable font variation axes

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


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

Unit tests for rendering and props

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


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

Accessibility compliance tests with axe-core

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


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

Auto-generated component API documentation

src/components/ankh-icon/readme.md


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

TypeScript type definitions for ankh-icon

src/components.d.ts


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

Architecture decision record for icon strategy

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


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

Update ADR index with new icon strategy record

docs/adr/README.md


9. package.json Dependencies +3/-1

Add jsdom and vitest-axe dev dependencies

package.json


Grey Divider

Qodo Logo

@imagineux imagineux marked this pull request as draft February 9, 2026 05:40
@qodo-code-review
Copy link

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. SemVer impact not documented 📘 Rule violation ✓ Correctness
Description
• The PR introduces a new public component API (<ankh-icon> with new props/types), but the PR
  description does not state the SemVer impact (patch/minor/major).
• This makes it harder for consumers to understand the release implications of the new public API
  surface.
Code

src/components/ankh-icon/ankh-icon.tsx[R9-41]

+@Component({
+  tag: 'ankh-icon',
+  styleUrl: 'ankh-icon.css',
+  shadow: false,
+})
+export class AnkhIcon {
+  /**
+   * Icon name from Material Symbols (e.g. "home", "settings", "favorite").
+   * Uses ligature resolution — the name is rendered as text content and
+   * the Material Symbols font converts it to the corresponding glyph.
+   */
+  @Prop() name!: string;
+
+  /**
+   * The rendered size of the icon.
+   * Maps to --icon-size-sm/md/lg/xl tokens with optical size tracking.
+   * @default 'md'
+   */
+  @Prop() size: IconSize = 'md';
+
+  /**
+   * Whether to render the filled variant of the icon.
+   * Controls the FILL axis of the Material Symbols variable font.
+   * @default false
+   */
+  @Prop() filled: boolean = false;
+
+  /**
+   * Accessible label for meaningful icons.
+   * When provided: sets role="img" and aria-label.
+   * When omitted: icon is treated as decorative (aria-hidden="true").
+   */
+  @Prop() label?: string;
Evidence
Compliance ID 8 requires documenting SemVer impact for public API changes. The diff adds a new
public component (ankh-icon), while the PR description/template does not include any SemVer impact
note.

Rule 8: SemVer impact documented
src/components/ankh-icon/ankh-icon.tsx[9-41]
.github/PULL_REQUEST_TEMPLATE.md[1-11]

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

## Issue description
A new public component API (`ankh-icon`) was added, but the PR does not document the SemVer impact (patch/minor/major).

## Issue Context
The repository PR template does not prompt authors for SemVer impact, so public API changes can be merged without consumers knowing release implications.

## Fix Focus Areas
- .github/PULL_REQUEST_TEMPLATE.md[1-11]
- README.md[90-95]

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


2. Root README missing ankh-icon 📘 Rule violation ✓ Correctness
Description
• The root README.md “Available Components” list and usage examples do not include the newly added
  <ankh-icon> component.
• This violates the requirement to update consumer-facing docs/examples when new component
  behavior/variants are introduced.
Code

src/components/ankh-icon/ankh-icon.tsx[R9-59]

+@Component({
+  tag: 'ankh-icon',
+  styleUrl: 'ankh-icon.css',
+  shadow: false,
+})
+export class AnkhIcon {
+  /**
+   * Icon name from Material Symbols (e.g. "home", "settings", "favorite").
+   * Uses ligature resolution — the name is rendered as text content and
+   * the Material Symbols font converts it to the corresponding glyph.
+   */
+  @Prop() name!: string;
+
+  /**
+   * The rendered size of the icon.
+   * Maps to --icon-size-sm/md/lg/xl tokens with optical size tracking.
+   * @default 'md'
+   */
+  @Prop() size: IconSize = 'md';
+
+  /**
+   * Whether to render the filled variant of the icon.
+   * Controls the FILL axis of the Material Symbols variable font.
+   * @default false
+   */
+  @Prop() filled: boolean = false;
+
+  /**
+   * Accessible label for meaningful icons.
+   * When provided: sets role="img" and aria-label.
+   * When omitted: icon is treated as decorative (aria-hidden="true").
+   */
+  @Prop() label?: string;
+
+  render() {
+    const isDecorative = !this.label;
+
+    return (
+      <Host class="ankh-icon-host">
+        <span
+          class={cn('ankh-icon', 'material-symbols-outlined', `ankh-icon--${this.size}`, this.filled && 'ankh-icon--filled')}
+          role={isDecorative ? undefined : 'img'}
+          aria-label={this.label || undefined}
+          aria-hidden={isDecorative ? 'true' : undefined}
+        >
+          {this.name}
+        </span>
+      </Host>
+    );
+  }
+}
Evidence
Compliance ID 24 requires documentation/examples to be updated when new component behavior or
variants are added. The PR adds a new public component (ankh-icon), but the root README component
list (consumer-facing docs) does not mention it.

src/components/ankh-icon/ankh-icon.tsx[9-55]
README.md[37-43]
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
The new `&lt;ankh-icon&gt;` component is not documented in the root `README.md` component list or usage section.

## Issue Context
Consumers rely on the root README for what components exist and how to use them; without adding `ankh-icon`, the new public API is effectively undiscoverable.

## Fix Focus Areas
- README.md[37-67]
- README.md[85-95]

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


3. Missing icon font-family 🐞 Bug ✓ Correctness
Description
• The ADR explicitly states the component sets font-family, but the implementation never sets it,
  so consumers who only load the font via @font-face (or don’t include Material’s CSS class
  definitions) can see the ligature text (e.g. "home") instead of a glyph.
• The component currently relies on an external material-symbols-outlined CSS class to set the
  font, but this requirement is not enforced or documented by the component itself.
• Impact: icons may silently fail in real consumer apps depending on how fonts/styles are loaded,
  and the component won’t meet the ADR’s stated contract.
Code

src/components/ankh-icon/ankh-icon.css[R12-22]

+.ankh-icon {
+  /* Composable font variation axes (private custom properties) */
+  --_fill: 0;
+  --_opsz: 24;
+
+  /* Material Symbols base */
+  font-variation-settings: 'FILL' var(--_fill), 'wght' 400, 'GRAD' 0, 'opsz' var(--_opsz);
+
+  /* Default size maps to md token */
+  font-size: var(--icon-size-md, 24px);
+
Evidence
The ADR documents that the component itself sets font-family, but the component CSS only sets
variation settings and sizing—no font-family is applied. Instead, the render output includes a
third-party class name (material-symbols-outlined) that would need external CSS to define the
correct font family.

docs/adr/002-icon-rendering-strategy.md[43-50]
src/components/ankh-icon/ankh-icon.css[12-22]
src/components/ankh-icon/ankh-icon.tsx[43-55]

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` is documented (ADR-002) as setting its own `font-family`, but the implementation never sets it. As a result, the component currently depends on external CSS for the `material-symbols-outlined` class to provide the correct font family.

## Issue Context
Consumers may load Material Symbols via `@font-face` without including Google’s helper CSS (or may not have a `.material-symbols-outlined` class available). In those setups, the icon will render as plain ligature text.

## Fix Focus Areas
- src/components/ankh-icon/ankh-icon.css[12-46]
- src/components/ankh-icon/ankh-icon.tsx[47-55]
- docs/adr/002-icon-rendering-strategy.md[43-50]

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



Remediation recommended

4. A11y test env inconsistency 🐞 Bug ⛯ Reliability
Description
• The repo’s Vitest default environment is happy-dom, but the new a11y spec forces jsdom,
  introducing a second DOM implementation with different behavior/supported APIs.
• The a11y spec also re-implements the “wait for hydration” logic inline instead of using the repo’s
  shared waitForHydration() helper, increasing duplication and the chance tests diverge over time.
• Impact: higher long-term maintenance cost and increased chance of environment-specific test
  breakage as component test utilities evolve.
Code

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

+/**
+ * @vitest-environment jsdom
+ */
+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';
+import './ankh-icon.js';
+
+declare module 'vitest' {
+  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
+  interface Assertion<T> extends AxeMatchers {}
+  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
+  interface AsymmetricMatchersContaining extends AxeMatchers {}
+}
+
+expect.extend(matchers);
+
+describe('ankh-icon a11y', () => {
+  let container: HTMLDivElement;
+
+  const createIcon = async (attrs: Record<string, string> = {}) => {
+    const el = document.createElement('ankh-icon');
+    Object.entries(attrs).forEach(([key, value]) => el.setAttribute(key, value));
+    container.appendChild(el);
+    await new Promise((resolve) => requestAnimationFrame(resolve));
+    return el;
+  };
Evidence
Vitest is configured to run under happy-dom globally, but the a11y spec explicitly overrides to
jsdom and implements its own rAF waiting. The repo already has a shared hydration helper that could
be reused for consistency.

vitest.config.ts[4-10]
src/components/ankh-icon/ankh-icon.a11y.spec.ts[1-28]
src/test-utils/component-helpers.ts[6-10]

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 `ankh-icon` a11y test runs under a different DOM environment (`jsdom`) than the rest of the suite (`happy-dom`) and duplicates hydration-wait logic inline.

## Issue Context
Mixing DOM environments is sometimes necessary (e.g., for tooling compatibility), but it should be done deliberately with shared helpers to avoid inconsistent patterns and duplicated setup.

## Fix Focus Areas
- src/components/ankh-icon/ankh-icon.a11y.spec.ts[1-28]
- vitest.config.ts[4-10]
- src/test-utils/component-helpers.ts[6-15]

ⓘ 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

Comment on lines +9 to +41
@Component({
tag: 'ankh-icon',
styleUrl: 'ankh-icon.css',
shadow: false,
})
export class AnkhIcon {
/**
* Icon name from Material Symbols (e.g. "home", "settings", "favorite").
* Uses ligature resolution — the name is rendered as text content and
* the Material Symbols font converts it to the corresponding glyph.
*/
@Prop() name!: string;

/**
* The rendered size of the icon.
* Maps to --icon-size-sm/md/lg/xl tokens with optical size tracking.
* @default 'md'
*/
@Prop() size: IconSize = 'md';

/**
* Whether to render the filled variant of the icon.
* Controls the FILL axis of the Material Symbols variable font.
* @default false
*/
@Prop() filled: boolean = false;

/**
* Accessible label for meaningful icons.
* When provided: sets role="img" and aria-label.
* When omitted: icon is treated as decorative (aria-hidden="true").
*/
@Prop() label?: string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Semver impact not documented 📘 Rule violation ✓ Correctness

• The PR introduces a new public component API (<ankh-icon> with new props/types), but the PR
  description does not state the SemVer impact (patch/minor/major).
• This makes it harder for consumers to understand the release implications of the new public API
  surface.
Agent Prompt
## Issue description
A new public component API (`ankh-icon`) was added, but the PR does not document the SemVer impact (patch/minor/major).

## Issue Context
The repository PR template does not prompt authors for SemVer impact, so public API changes can be merged without consumers knowing release implications.

## Fix Focus Areas
- .github/PULL_REQUEST_TEMPLATE.md[1-11]
- README.md[90-95]

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

Comment on lines +9 to +59
@Component({
tag: 'ankh-icon',
styleUrl: 'ankh-icon.css',
shadow: false,
})
export class AnkhIcon {
/**
* Icon name from Material Symbols (e.g. "home", "settings", "favorite").
* Uses ligature resolution — the name is rendered as text content and
* the Material Symbols font converts it to the corresponding glyph.
*/
@Prop() name!: string;

/**
* The rendered size of the icon.
* Maps to --icon-size-sm/md/lg/xl tokens with optical size tracking.
* @default 'md'
*/
@Prop() size: IconSize = 'md';

/**
* Whether to render the filled variant of the icon.
* Controls the FILL axis of the Material Symbols variable font.
* @default false
*/
@Prop() filled: boolean = false;

/**
* Accessible label for meaningful icons.
* When provided: sets role="img" and aria-label.
* When omitted: icon is treated as decorative (aria-hidden="true").
*/
@Prop() label?: string;

render() {
const isDecorative = !this.label;

return (
<Host class="ankh-icon-host">
<span
class={cn('ankh-icon', 'material-symbols-outlined', `ankh-icon--${this.size}`, this.filled && 'ankh-icon--filled')}
role={isDecorative ? undefined : 'img'}
aria-label={this.label || undefined}
aria-hidden={isDecorative ? 'true' : undefined}
>
{this.name}
</span>
</Host>
);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Root readme missing ankh-icon 📘 Rule violation ✓ Correctness

• The root README.md “Available Components” list and usage examples do not include the newly added
  <ankh-icon> component.
• This violates the requirement to update consumer-facing docs/examples when new component
  behavior/variants are introduced.
Agent Prompt
## Issue description
The new `<ankh-icon>` component is not documented in the root `README.md` component list or usage section.

## Issue Context
Consumers rely on the root README for what components exist and how to use them; without adding `ankh-icon`, the new public API is effectively undiscoverable.

## Fix Focus Areas
- README.md[37-67]
- README.md[85-95]

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

Comment on lines +12 to +22
.ankh-icon {
/* Composable font variation axes (private custom properties) */
--_fill: 0;
--_opsz: 24;

/* Material Symbols base */
font-variation-settings: 'FILL' var(--_fill), 'wght' 400, 'GRAD' 0, 'opsz' var(--_opsz);

/* Default size maps to md token */
font-size: var(--icon-size-md, 24px);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Missing icon font-family 🐞 Bug ✓ Correctness

• The ADR explicitly states the component sets font-family, but the implementation never sets it,
  so consumers who only load the font via @font-face (or don’t include Material’s CSS class
  definitions) can see the ligature text (e.g. "home") instead of a glyph.
• The component currently relies on an external material-symbols-outlined CSS class to set the
  font, but this requirement is not enforced or documented by the component itself.
• Impact: icons may silently fail in real consumer apps depending on how fonts/styles are loaded,
  and the component won’t meet the ADR’s stated contract.
Agent Prompt
## Issue description
`ankh-icon` is documented (ADR-002) as setting its own `font-family`, but the implementation never sets it. As a result, the component currently depends on external CSS for the `material-symbols-outlined` class to provide the correct font family.

## Issue Context
Consumers may load Material Symbols via `@font-face` without including Google’s helper CSS (or may not have a `.material-symbols-outlined` class available). In those setups, the icon will render as plain ligature text.

## Fix Focus Areas
- src/components/ankh-icon/ankh-icon.css[12-46]
- src/components/ankh-icon/ankh-icon.tsx[47-55]
- docs/adr/002-icon-rendering-strategy.md[43-50]

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

@imagineux
Copy link
Contributor Author

/review

@qodo-code-review
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Prop Typing

Several tests set the boolean filled prop via the string value "true". This can accidentally test attribute parsing behavior instead of the component’s boolean prop handling, and could mask regressions depending on how createElement maps values to properties vs attributes.

describe('filled prop', () => {
  it('applies ankh-icon--filled class when filled is set', async () => {
    const el = await createElement<HTMLElement>('ankh-icon', container, { name: 'favorite', filled: 'true' });
    const span = el.querySelector('span.ankh-icon');
    expect(span!.classList.contains('ankh-icon--filled')).toBe(true);
  });

  it('does not apply filled class when filled is not set', async () => {
    const el = await createElement<HTMLElement>('ankh-icon', container, { name: 'favorite' });
    const span = el.querySelector('span.ankh-icon');
    expect(span!.classList.contains('ankh-icon--filled')).toBe(false);
  });

  it('applies filled and size classes together', async () => {
    const el = await createElement<HTMLElement>('ankh-icon', container, { name: 'favorite', filled: 'true', size: 'xl' });
    const span = el.querySelector('span.ankh-icon');
    expect(span!.classList.contains('ankh-icon--filled')).toBe(true);
    expect(span!.classList.contains('ankh-icon--xl')).toBe(true);
  });
Input Validation

name is required but the component currently allows empty strings and renders an empty glyph. Consider whether an empty/whitespace-only name should be treated as an error (warn in dev), have a fallback, or be prevented to avoid silent failures/typos.

/**
 * Icon name from Material Symbols (e.g. "home", "settings", "favorite").
 * Uses ligature resolution — the name is rendered as text content and
 * the Material Symbols font converts it to the corresponding glyph.
 */
@Prop() name!: string;

/**
 * The rendered size of the icon.
 * Maps to --icon-size-sm/md/lg/xl tokens with optical size tracking.
 * @default 'md'
 */
@Prop() size: IconSize = 'md';

/**
 * Whether to render the filled variant of the icon.
 * Controls the FILL axis of the Material Symbols variable font.
 * @default false
 */
@Prop() filled: boolean = false;

/**
 * Accessible label for meaningful icons.
 * When provided: sets role="img" and aria-label.
 * When omitted: icon is treated as decorative (aria-hidden="true").
 */
@Prop() label?: string;

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}
      >
        {this.name}
      </span>
Font Fallback

font-family is set to Material Symbols without a fallback. If the consumer forgets to load the font (or it fails), the ligature text will render visibly. Consider adding a sensible fallback stack or optional styling guidance to minimize the chance of raw icon names appearing in UI.

.ankh-icon {
  /* Composable font variation axes (private custom properties) */
  --_fill: 0;
  --_opsz: 24;

  /* Material Symbols base — component sets the family per ADR-002;
     consumer loads the @font-face, we just reference it here. */
  font-family: 'Material Symbols Outlined';
  font-variation-settings: 'FILL' var(--_fill), 'wght' 400, 'GRAD' 0, 'opsz' var(--_opsz);

  /* Default size maps to md token */
  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';
}

@imagineux imagineux closed this Feb 18, 2026
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