Files
web/scripts/material-symbols-update.mts
Linus Flood cd59102ef4 Merged in feat/svg-instead-of-fonts (pull request #3411)
feat(SW-3695): use svg icons instead of font icons

* feat(icons): use svg instead of font icons

* feat(icons): use webpack/svgr for inlined svgs. Now support for isFilled again

* Merge master

* Remove old font icon


Approved-by: Joakim Jäderberg
2026-01-09 13:14:09 +00:00

324 lines
6.1 KiB
TypeScript

import { writeFile } from "node:fs/promises"
import { resolve } from "node:path"
import { existsSync } from "node:fs"
// Your full list of Material Symbol icon names
const ICONS = [
"accessibility",
"accessible",
"acute",
"add_circle",
"add",
"air_purifier_gen",
"air",
"airline_seat_recline_normal",
"airplane_ticket",
"apartment",
"apparel",
"arrow_back_ios",
"arrow_back",
"arrow_forward_ios",
"arrow_forward",
"arrow_right",
"arrow_upward",
"assistant_navigation",
"asterisk",
"attractions",
"award_star",
"bakery_dining",
"balcony",
"bathroom",
"bathtub",
"beach_access",
"bed",
"bedroom_parent",
"box",
"brunch_dining",
"business_center",
"calendar_add_on",
"calendar_clock",
"calendar_month",
"calendar_today",
"call_quality",
"call",
"camera",
"cancel",
"chair",
"mobile_charge",
"check_box",
"check_circle",
"check",
"checked_bag",
"checkroom",
"chevron_left",
"chevron_right",
"close",
"coffee_maker",
"coffee",
"compare_arrows",
"computer",
"concierge",
"confirmation_number",
"connected_tv",
"content_copy",
"contract",
"cool_to_dry",
"countertops",
"credit_card_heart",
"credit_card",
"credit_score",
"curtains_closed",
"curtains",
"dangerous",
"deck",
"delete",
"desk",
"device_thermostat",
"diamond",
"dining",
"directions_run",
"directions_subway",
"directions",
"downhill_skiing",
"download",
"dresser",
"edit_calendar",
"edit_square",
"edit",
"electric_bike",
"electric_car",
"elevator",
"emoji_transportation",
"error",
"exercise",
"family_restroom",
"fastfood",
"favorite",
"fax",
"featured_seasonal_and_gifts",
"festival",
"filter_alt",
"filter",
"format_list_bulleted",
"floor_lamp",
"forest",
"garage",
"globe",
"golf_course",
"groups",
"health_and_beauty",
"heat",
"hiking",
"home",
"hot_tub",
"houseboat",
"hvac",
"id_card",
"imagesmode",
"info",
"iron",
"kayaking",
"kettle",
"keyboard_arrow_down",
"keyboard_arrow_right",
"keyboard_arrow_up",
"king_bed",
"kitchen",
"landscape",
"laundry",
"link",
"liquor",
"live_tv",
"local_bar",
"local_cafe",
"local_convenience_store",
"local_drink",
"local_laundry_service",
"local_parking",
"location_city",
"location_on",
"lock",
"lock_clock",
"loyalty",
"luggage",
"mail",
"map",
"meeting_room",
"microwave",
"mode_fan",
"museum",
"music_cast",
"music_note",
"nature",
"night_shelter",
"nightlife",
"open_in_new",
"pan_zoom",
"panorama",
"pause",
"pedal_bike",
"person",
"pets",
"phone_enabled",
"photo_camera",
"play_arrow",
"pool",
"print",
"radio",
"recommend",
"redeem",
"refresh",
"remove",
"restaurant",
"room_service",
"router",
"sailing",
"sauna",
"scene",
"search",
"sell",
"shopping_bag",
"shower",
"single_bed",
"skateboarding",
"smoke_free",
"smoking_rooms",
"spa",
"sports_esports",
"sports_golf",
"sports_handball",
"sports_tennis",
"stairs",
"star",
"sticky_note_2",
"straighten",
"styler",
"support_agent",
"swipe",
"sync_saved_locally",
"table_bar",
"theater_comedy",
"things_to_do",
"train",
"tram",
"transit_ticket",
"travel_luggage_and_bags",
"travel",
"trophy",
"tv_guide",
"tv_remote",
"upload",
"visibility_off",
"visibility",
"volume_off",
"volume_up",
"ward",
"warning",
"water_full",
"wifi",
"yard",
].sort()
const STYLES = ["outlined", "rounded", "sharp"] as const
const OUT = resolve(
__dirname,
"../packages/design-system/lib/components/Icons/MaterialIcon/generated.tsx"
)
const PACKAGE_BASE = resolve(
__dirname,
"../node_modules/@material-symbols/svg-400"
)
function camelCase(str: string) {
return str.replace(/[-_](\w)/g, (_, c: string) => c.toUpperCase())
}
async function main() {
const imports: string[] = []
const registryEntries: string[] = []
const missing: string[] = []
for (const icon of ICONS) {
const styleEntries: string[] = []
for (const style of STYLES) {
const parts: string[] = []
const fill0Path = resolve(PACKAGE_BASE, style, `${icon}.svg`)
const fill1Path = resolve(PACKAGE_BASE, style, `${icon}-fill.svg`)
let outlinedVar: string | undefined
let filledVar: string | undefined
if (existsSync(fill0Path)) {
outlinedVar = `${camelCase(icon)}${camelCase(style)}Outlined`
imports.push(
`import ${outlinedVar} from "@material-symbols/svg-400/${style}/${icon}.svg"`
)
parts.push(`outlined: ${outlinedVar}`)
} else {
missing.push(`${style}/${icon}.svg`)
}
if (existsSync(fill1Path)) {
filledVar = `${camelCase(icon)}${camelCase(style)}Filled`
imports.push(
`import ${filledVar} from "@material-symbols/svg-400/${style}/${icon}-fill.svg"`
)
parts.push(`filled: ${filledVar}`)
}
if (parts.length) {
styleEntries.push(`${style}: { ${parts.join(", ")} }`)
}
}
// ALWAYS emit the icon key (even if partial)
if (styleEntries.length) {
registryEntries.push(`"${icon}": { ${styleEntries.join(", ")} },`)
} else {
missing.push(`❌ no variants for "${icon}"`)
}
}
const content =
`
/* AUTO-GENERATED — DO NOT EDIT */
import type { FunctionComponent, SVGProps } from "react"
${imports.join("\n")}
type SvgIcon = FunctionComponent<SVGProps<SVGSVGElement>>
export const materialIcons: Record<
string,
Partial<{
outlined: { outlined: SvgIcon; filled?: SvgIcon }
rounded: { outlined: SvgIcon; filled?: SvgIcon }
sharp: { outlined: SvgIcon; filled?: SvgIcon }
}>
> = {
${registryEntries.join("\n")}
}
export type MaterialIconName = keyof typeof materialIcons
`.trim() + "\n"
await writeFile(OUT, content, "utf8")
console.log(`✅ Generated ${registryEntries.length} icons`)
if (missing.length) {
console.warn("⚠️ Missing SVGs:")
missing.slice(0, 20).forEach((m) => console.warn(" ", m))
if (missing.length > 20) {
console.warn(` …and ${missing.length - 20} more`)
}
}
}
main().catch(console.error)