Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6e0a2f3
docs: remove legacy angularjs documentation site (#2815)
Yashh56 Nov 1, 2026
4173712
feat(site): add modern sveltekit documentation website in site/ (#2815)
Yashh56 Nov 1, 2026
dc92e1f
chore(site): add root npm scripts and update local development docs (…
Yashh56 Nov 1, 2026
7110d05
ci(site): add workflows for pr validation and pages deployment (#2815)
Yashh56 Nov 1, 2026
b38e521
Merge branch 'develop' into feat/website
Yashh56 Nov 1, 2026
1455daa
fix(site): configure pnpm allowBuilds for esbuild (#2815)
Yashh56 Nov 1, 2026
91df5e8
Merge branch 'develop' into feat/website
Yashh56 Nov 1, 2026
15c433d
feat(site): update to devicon v2.18.0 and add contributors section in…
Yashh56 Nov 1, 2026
104972d
feat(site): add pure raw svg viewing, copying, and download support (…
Yashh56 Nov 1, 2026
efe77da
feat(site): format svg markup, align snippet to start, and hide scrol…
Yashh56 Nov 1, 2026
6e2095b
Merge branch 'develop' into feat/website
Yashh56 Nov 1, 2026
680b3b1
feat(site): overhaul visual identity, typography, navigation, and her…
Yashh56 Nov 2, 2026
f231085
feat(site): enhance icon explorer, filtering toolbar, and card SVG re…
Yashh56 Nov 2, 2026
1557684
feat(site): support all canonical icon variants, alias resolution, an…
Yashh56 Nov 2, 2026
64ed7ed
feat(site): streamline framework integrations and CDN section (#2815)
Yashh56 Nov 2, 2026
a253d7b
Merge branch 'develop' into feat/website
Yashh56 Nov 2, 2026
3f62960
feat(site): refine typography with Geist Sans and Geist Mono (#2815)
Yashh56 Nov 2, 2026
fc8b254
feat(site): integrate Steve Ballmer media card into hero window frame…
Yashh56 Nov 2, 2026
fdb4499
Merge branch 'feat/website' of https://github.com/Yashh56/devicon int…
Yashh56 Nov 2, 2026
b6c8320
fix(site): rename to ballmer.gif and bind base path to prevent 404 on…
Yashh56 Nov 2, 2026
1e22b56
feat(site): add ballmer.gif to static assets
Yashh56 Nov 3, 2026
d45a4b2
feat(site): implement automated contrast system and local icon asset …
Yashh56 Nov 3, 2026
b513836
feat(site): elevate brand personality with Devicon green tokens, Tech…
Yashh56 Nov 3, 2026
3a6e471
Merge branch 'develop' into feat/website
Yashh56 Nov 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 158 additions & 37 deletions .github/scripts/check_icon_pr.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,85 @@
import colorsys
from pathlib import Path
import re
from typing import List
import xml.etree.ElementTree as et
from pathlib import Path


# pycharm complains that build_assets is an unresolved ref
# don't worry about it, the script still runs
from build_assets import filehandler, arg_getters, util
from build_assets import arg_getters, filehandler, util


def hex_to_rgb(hex_str: str):
cleaned = hex_str.lstrip("#").strip()
if len(cleaned) in (3, 4):
cleaned = "".join(c * 2 for c in cleaned[:3])
elif len(cleaned) >= 6:
cleaned = cleaned[:6]
else:
return None
try:
val = int(cleaned, 16)
return (
((val >> 16) & 255) / 255.0,
((val >> 8) & 255) / 255.0,
(val & 255) / 255.0,
)
except ValueError:
return None


def is_color_saturated(r: float, g: float, b: float, threshold: float = 0.20) -> bool:
_, _, s = colorsys.rgb_to_hls(r, g, b)
return s >= threshold


def svg_contains_saturated_colors(svg_path: Path) -> bool:
try:
content = svg_path.read_text(encoding="utf-8")
except Exception:
return False

# Check hex colors: #rgb, #rrggbb, etc.
hex_matches = re.findall(r"#([0-9a-fA-F]{3,8})\b", content)
for h in hex_matches:
rgb = hex_to_rgb(h)
if rgb and is_color_saturated(*rgb):
return True

# Check rgb(...) colors
rgb_matches = re.findall(
r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)", content
)
for r_s, g_s, b_s in rgb_matches:
rgb = (int(r_s) / 255.0, int(g_s) / 255.0, int(b_s) / 255.0)
if is_color_saturated(*rgb):
return True

# Check hsl(...) colors
hsl_matches = re.findall(
r"hsl\(\s*[\d.]+\s*,\s*([\d.]+)%\s*,\s*[\d.]+%?\s*\)", content
)
for s_s in hsl_matches:
try:
if float(s_s) / 100.0 >= 0.20:
return True
except ValueError:
pass

return False


def icon_svgs_have_saturated_colors(icon: dict, icons_folder_path="./icons") -> bool:
if not icon or "name" not in icon:
return False
icon_dir = Path(icons_folder_path) / icon["name"]
if not icon_dir.is_dir():
return False

for svg_file in icon_dir.glob("*.svg"):
if svg_contains_saturated_colors(svg_file):
return True
return False


def main():
Expand All @@ -19,28 +93,36 @@ def main():
all_icons = filehandler.get_json_file_content(args.devicon_json_path)

devicon_err_msg = []
#First check if devicon.json is sorted
if sorted(all_icons, key=lambda d: d['name']) != all_icons:
devicon_err_msg.append(f"devicon.json is not sorted correctly.\nPlease make sure that your icon is added in the `devicon.json` file at the correct alphabetic position\nas seen here: https://github.com/devicons/devicon/wiki/Updating-%60devicon.json%60")
# First check if devicon.json is sorted
if sorted(all_icons, key=lambda d: d["name"]) != all_icons:
devicon_err_msg.append(
"devicon.json is not sorted correctly.\n"
"Please make sure that your icon is added in the `devicon.json` file at the correct alphabetic position\n"
"as seen here: https://github.com/devicons/devicon/wiki/Updating-%60devicon.json%60"
)

# get only the icon object that has the name matching the pr title
filtered_icon = util.find_object_added_in_pr(all_icons, args.pr_title)
print("Checking devicon.json object: " + str(filtered_icon))
devicon_err_msg.append(check_devicon_object(filtered_icon))
devicon_err_msg.append(check_devicon_object(filtered_icon, args.icons_folder_path))

# check the file names
filename_err_msg = ""
svgs = None
try:
svgs = filehandler.get_svgs_paths([filtered_icon], args.icons_folder_path, as_str=False)
print("SVGs to check: ", *svgs, sep='\n')
svgs = filehandler.get_svgs_paths(
[filtered_icon], args.icons_folder_path, as_str=False
)
print("SVGs to check: ", *svgs, sep="\n")
except ValueError as e:
filename_err_msg = "Error found regarding filenames:\n- " + e.args[0]

# check the svgs
if svgs is None or len(svgs) == 0:
print("No SVGs to check, ending script.")
svg_err_msg = "Error checking SVGs: no SVGs to check. Might be caused by above issues."
svg_err_msg = (
"Error checking SVGs: no SVGs to check. Might be caused by above issues."
)
else:
svg_err_msg = check_svgs(svgs, filtered_icon)

Expand All @@ -61,7 +143,7 @@ def main():
util.exit_with_err(e)


def check_devicon_object(icon: dict):
def check_devicon_object(icon: dict, icons_folder_path="./icons"):
"""
Check that the devicon object added is up to standard.
:return a string containing the error messages if any.
Expand All @@ -70,27 +152,33 @@ def check_devicon_object(icon: dict):

try:
if type(icon["name"]) != str:
err_msgs.append("- Property \"name\" value must be type string.")
err_msgs.append('- Property "name" value must be type string.')
except KeyError:
err_msgs.append("- Missing property key: \"name\".")
err_msgs.append('- Missing property key: "name".')

try:
for altname in icon["altnames"]:
if type(altname) != str:
raise TypeError()
if altname == icon["name"]:
err_msgs.append(f"- \"altnames\" should not contain the same name as \"name\" property. Please remove \"{altname}\" from \"altnames\"")
err_msgs.append(
f'- "altnames" should not contain the same name as "name" property. Please remove "{altname}" from "altnames"'
)
except TypeError:
err_msgs.append("- \"altnames\" must be an array of strings, not: " + str(icon["altnames"]))
err_msgs.append(
'- "altnames" must be an array of strings, not: ' + str(icon["altnames"])
)
except KeyError:
err_msgs.append("- Missing property key: \"altnames\".")
err_msgs.append('- Missing property key: "altnames".')

try:
for tag in icon["tags"]:
if type(tag) != str:
raise TypeError()
except TypeError:
err_msgs.append("- 'tags' must be an array of strings, not: " + str(icon["tags"]))
err_msgs.append(
"- 'tags' must be an array of strings, not: " + str(icon["tags"])
)
except KeyError:
err_msgs.append("- missing key: 'tags'.")

Expand All @@ -101,66 +189,93 @@ def check_devicon_object(icon: dict):
err_msgs.append("- missing key: 'versions'.")

try:
if type(icon["versions"]["svg"]) != list or len(icon["versions"]["svg"]) == 0:
if (
type(icon["versions"]["svg"]) != list
or len(icon["versions"]["svg"]) == 0
):
err_msgs.append("- must contain at least 1 svg version in a list.")

for version in icon["versions"]["svg"]:
if not util.is_svg_version_valid(version):
err_msgs.append(f"- Invalid version name in versions['svg']: '{version}'. Must match regexp: (original|plain|line)(-wordmark)?")
err_msgs.append(
f"- Invalid version name in versions['svg']: '{version}'. Must match regexp: (original|plain|line)(-wordmark)?"
)
except KeyError:
err_msgs.append("- missing key: 'svg' in 'versions'.")

try:
if type(icon["versions"]["font"]) != list or len(icon["versions"]["svg"]) == 0:
if (
type(icon["versions"]["font"]) != list
or len(icon["versions"]["svg"]) == 0
):
err_msgs.append("- must contain at least 1 font version in a list.")

for version in icon["versions"]["font"]:
if not util.is_svg_version_valid(version):
err_msgs.append(f"- Invalid version name in versions['font']: '{version}'. Must match regexp: (original|plain|line)(-wordmark)?")
err_msgs.append(
f"- Invalid version name in versions['font']: '{version}'. Must match regexp: (original|plain|line)(-wordmark)?"
)
except KeyError:
err_msgs.append("- missing key: 'font' in 'versions'.")

try:
if type(icon["color"]) != str or "#" not in icon["color"]:
color = icon["color"]
if type(color) != str or "#" not in color:
err_msgs.append("- 'color' must be a string in the format '#abcdef'")
else:
normalized_color = color.strip().lower()
if normalized_color in ["#000000", "#000", "#ffffff"]:
if icon_svgs_have_saturated_colors(icon, icons_folder_path):
err_msgs.append(
f"- 'color' property '{color}' appears to be a placeholder, but the icon's SVG contains colored fills/strokes. "
"Please specify the official brand color (e.g. hex code '#abcdef') instead of a placeholder."
)
except KeyError:
err_msgs.append("- missing key: 'color'.")

try:
if type(icon["aliases"]) != list:
err_msgs.append("- \"aliases\" must be an array of objects.")
err_msgs.append('- "aliases" must be an array of objects.')
except KeyError:
err_msgs.append("- missing key: 'aliases'.")
err_msgs.append('- missing key: "aliases".')

try:
for alias_objects in icon["aliases"]:
if type(alias_objects) != dict:
raise TypeError()
except TypeError:
err_msgs.append("- \"aliases\" must be an array of objects, not: " + str(icon["aliases"]))
err_msgs.append(
'- "aliases" must be an array of objects, not: ' + str(icon["aliases"])
)

try:
for alias_objects in icon["aliases"]:
if type(alias_objects["base"]) != str:
err_msgs.append("- must contain at least 1 base in aliases.")
if not util.is_svg_version_valid(alias_objects['base']):
err_msgs.append(f"- Invalid base name in aliases[\"base\"]: \"{alias_objects['base']}\". Must match regexp: (original|plain|line)(-wordmark)?")
if not util.is_svg_version_valid(alias_objects["base"]):
err_msgs.append(
f'- Invalid base name in aliases["base"]: "{alias_objects["base"]}". Must match regexp: (original|plain|line)(-wordmark)?'
)
except KeyError:
err_msgs.append("- missing key: \"base\" in \"aliases\".")
err_msgs.append('- missing key: "base" in "aliases".')

try:
for alias_objects in icon["aliases"]:
if type(alias_objects["alias"]) != str:
err_msgs.append("- must contain at least 1 alias in aliases.")
if not util.is_svg_version_valid(alias_objects['alias']):
err_msgs.append(f"- Invalid alias name in aliases['alias']: \"{alias_objects['alias']}\". Must match regexp: (original|plain|line)(-wordmark)?")
if not util.is_svg_version_valid(alias_objects["alias"]):
err_msgs.append(
f"- Invalid alias name in aliases['alias']: \"{alias_objects['alias']}\". Must match regexp: (original|plain|line)(-wordmark)?"
)
except KeyError:
err_msgs.append("- missing key: \"alias\" in \"aliases\".")
err_msgs.append('- missing key: "alias" in "aliases".')

if len(err_msgs) > 0:
message = "Error found in \"devicon.json\" for \"{}\" entry: \n{}".format(icon["name"], "\n".join(err_msgs))
message = 'Error found in "devicon.json" for "{}" entry: \n{}'.format(
icon["name"], "\n".join(err_msgs)
)
return message
return ""
return ""


def check_svgs(svg_file_paths: List[Path], devicon_object: dict):
Expand All @@ -181,7 +296,9 @@ def check_svgs(svg_file_paths: List[Path], devicon_object: dict):

# name check
if not util.is_svg_name_valid(svg_path.name):
err_msg.append("- SVG file name didn't match our pattern of `name-(original|plain|line)(-wordmark)?.svg`")
err_msg.append(
"- SVG file name didn't match our pattern of `name-(original|plain|line)(-wordmark)?.svg`"
)

# svg check
tree = et.parse(svg_path)
Expand All @@ -192,13 +309,17 @@ def check_svgs(svg_file_paths: List[Path], devicon_object: dict):
err_msg.append(f"- root is '{root.tag}'. Root must be an 'svg' element")

if root.get("viewBox") != "0 0 128 128":
err_msg.append("- 'viewBox' is not '0 0 128 128' -> Set it or scale the file using https://www.iloveimg.com/resize-image/resize-svg.")
err_msg.append(
"- 'viewBox' is not '0 0 128 128' -> Set it or scale the file using https://www.iloveimg.com/resize-image/resize-svg."
)

# goes through all elems and check for strokes
if util.is_svg_in_font_attribute(svg_path, devicon_object):
for child in tree.iter():
if child.get("stroke") != None:
err_msg.append("- SVG contains `stroke` property. This will get ignored by Icomoon. Please convert them to fills.")
err_msg.append(
"- SVG contains `stroke` property. This will get ignored by Icomoon. Please convert them to fills."
)
break

if len(err_msg) > 1:
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/check_site_pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Check Website PR

on:
pull_request:
paths:
- 'site/**'
- '.github/workflows/check_site_pr.yml'

jobs:
check:
name: Typecheck & Build Website
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '20'

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest

- name: Install dependencies
run: pnpm --dir site install --frozen-lockfile

- name: Svelte check
run: pnpm --dir site check

- name: Lint and format check
run: pnpm --dir site lint

- name: Build static site
run: pnpm --dir site build
Loading
Loading