From da21528ead43b276a40233bcbadc032e82d23964 Mon Sep 17 00:00:00 2001 From: Asim M Al Twijry Date: Fri, 1 May 2026 22:16:28 +0300 Subject: [PATCH] fix(icon): render non-path SVG primitives and preserve stroke caps/joins Two related bugs caused several Hugeicons to render incorrectly in Angular: 1. Non-path elements (`circle`, `ellipse`, `rect`, `line`, `polyline`, `polygon`) were rendered as empty `` because `paths()` only extracted `d/fill/opacity/fillRule` and the template emitted a single `` regardless of the source tag. Affects e.g. `CompassIcon` (the outer circle), `AlertCircleIcon` (the outer circle), `MapsLocation01Icon` (the inner pin dot). 2. `strokeLinecap` and `strokeLinejoin` were stripped from attrs. Several icons draw dots as near-zero-length paths (e.g. `Alert02Icon`'s exclamation dot is `M11.992 16H12.001`); without `stroke-linecap: round` the default `butt` linecap collapses them to nothing. Adds an `elementToPathD()` helper that converts each non-path SVG primitive into an equivalent path `d` string, and preserves linecap/linejoin (defaulting to `round`) when extracting attributes. The template already renders a single ``, so no DOM-shape changes. Closes #5 --- CHANGELOG.md | 8 ++ src/components/hugeicons-icon.component.ts | 89 ++++++++++++++++++---- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 816b006..98dcf13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # @hugeicons/angular +## Unreleased + +### Patch Changes + +- Fixed icons that use non-`path` SVG primitives (`circle`, `ellipse`, `rect`, `line`, `polyline`, `polygon`) rendering as empty paths. Affects e.g. `CompassIcon`, `AlertCircleIcon`, `MapsLocation01Icon`. Closes #5. +- Fixed near-zero-length paths (icon dots) being invisible by preserving `strokeLinecap` / `strokeLinejoin` (defaulting to `round`) when extracting attributes. + + ## 1.0.7 ### Patch Changes diff --git a/src/components/hugeicons-icon.component.ts b/src/components/hugeicons-icon.component.ts index aaf9dc6..b16fed7 100644 --- a/src/components/hugeicons-icon.component.ts +++ b/src/components/hugeicons-icon.component.ts @@ -2,12 +2,64 @@ import { Component, ChangeDetectionStrategy, computed, input } from '@angular/co import { IconSvgObject } from '../lib/types'; interface PathData { - d: string; + d?: string; fill: string; opacity?: string; fillRule?: string; stroke?: string; - strokeWidth?: number; + strokeWidth?: number | string; + strokeLinecap?: string; + strokeLinejoin?: string; +} + +type SvgAttrs = Record; + +/** + * Convert a non-`path` SVG primitive into an equivalent path `d` string so the + * single `` template can render every shape Hugeicons emits. + * + * Without this, icons that contain `circle`, `ellipse`, `rect`, `line`, + * `polyline`, or `polygon` lose those elements (they have no `d` attribute and + * collapse to an empty path). Affects e.g. `CompassIcon`, `AlertCircleIcon`, + * `MapsLocation01Icon`. See https://github.com/hugeicons/angular/issues/5 + */ +function elementToPathD(tag: string, attrs: SvgAttrs): string | undefined { + if (tag === 'path') return attrs['d'] as string | undefined; + + if (tag === 'circle') { + const cx = Number(attrs['cx']); + const cy = Number(attrs['cy']); + const r = Number(attrs['r']); + return `M ${cx - r} ${cy} a ${r} ${r} 0 1 0 ${2 * r} 0 a ${r} ${r} 0 1 0 ${-2 * r} 0`; + } + + if (tag === 'ellipse') { + const cx = Number(attrs['cx']); + const cy = Number(attrs['cy']); + const rx = Number(attrs['rx']); + const ry = Number(attrs['ry']); + return `M ${cx - rx} ${cy} a ${rx} ${ry} 0 1 0 ${2 * rx} 0 a ${rx} ${ry} 0 1 0 ${-2 * rx} 0`; + } + + if (tag === 'rect') { + const x = Number(attrs['x'] ?? 0); + const y = Number(attrs['y'] ?? 0); + const w = Number(attrs['width']); + const h = Number(attrs['height']); + return `M ${x} ${y} h ${w} v ${h} h ${-w} Z`; + } + + if (tag === 'line') { + return `M ${attrs['x1']} ${attrs['y1']} L ${attrs['x2']} ${attrs['y2']}`; + } + + if (tag === 'polyline' || tag === 'polygon') { + const points = String(attrs['points'] ?? '').trim(); + if (!points) return attrs['d'] as string | undefined; + return 'M ' + points.replace(/\s+/g, ' ').replace(/,/g, ' ') + (tag === 'polygon' ? ' Z' : ''); + } + + return attrs['d'] as string | undefined; } @Component({ @@ -31,6 +83,8 @@ interface PathData { [attr.fill-rule]="path.fillRule" [attr.stroke]="path.stroke" [attr.stroke-width]="path.strokeWidth" + [attr.stroke-linecap]="path.strokeLinecap" + [attr.stroke-linejoin]="path.strokeLinejoin" /> } @@ -61,21 +115,28 @@ export class HugeiconsIconComponent { const strokeWidthValue = this.strokeWidth(); const calculatedStrokeWidth = strokeWidthValue !== undefined - ? (this.absoluteStrokeWidth() - ? (Number(strokeWidthValue) * 24) / Number(this.size()) + ? (this.absoluteStrokeWidth() + ? (Number(strokeWidthValue) * 24) / Number(this.size()) : strokeWidthValue) : undefined; - const strokeProps = calculatedStrokeWidth !== undefined - ? { strokeWidth: calculatedStrokeWidth, stroke: 'currentColor' } + const strokeProps = calculatedStrokeWidth !== undefined + ? { strokeWidth: calculatedStrokeWidth, stroke: 'currentColor' } : {}; - return currentIcon.map(([_, attrs]) => ({ - d: attrs['d'], - fill: attrs['fill'] || 'none', - opacity: attrs['opacity'], - fillRule: attrs['fillRule'], - ...strokeProps - })); + return currentIcon.map(([tag, attrs]) => { + const a = attrs as SvgAttrs; + return { + d: elementToPathD(tag, a), + fill: (a['fill'] as string | undefined) || 'none', + opacity: a['opacity'] as string | undefined, + fillRule: a['fillRule'] as string | undefined, + // Round caps/joins are required for near-zero-length paths (dots) + // to render. The default `butt` linecap collapses them to nothing. + strokeLinecap: (a['strokeLinecap'] as string | undefined) ?? 'round', + strokeLinejoin: (a['strokeLinejoin'] as string | undefined) ?? 'round', + ...strokeProps, + }; + }); }); -} \ No newline at end of file +}