Browse Source

feat: FA Pro kit → SVG sprite pipeline (dw_icon helper)

bin/build-icons.mjs combines the FA Pro kit's pre-built per-style sprites
(@awesome.me/kit-*) into one namespaced icons.svg (40 icons, ~41 KB) — wired into
`npm run build` + `npm run icons`. inc/template.php adds dw_icon('name') (→ <svg
class=dw-icon><use href=#dw-name>) and injects the sprite once on wp_footer.
.dw-icon CSS sizes it like a font glyph. Verified: helper output, sprite injection,
build. This replaces the 689 KB FA webfont once the markup is migrated (next).
windhamdavid 2 days ago
parent
commit
90a0275d3b
7 changed files with 165 additions and 1 deletions
  1. 49 0
      bin/build-icons.mjs
  2. 9 0
      css/styles.scss
  3. 41 0
      icons.svg
  4. 28 0
      inc/template.php
  5. 35 0
      package-lock.json
  6. 3 1
      package.json
  7. 0 0
      v4-style.min.css

+ 49 - 0
bin/build-icons.mjs

@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+/**
+ * Build icons.svg — one inline SVG sprite combined from the Font Awesome Pro kit's
+ * pre-built per-style sprites under node_modules/@awesome.me/kit-<id>/icons/sprites/.
+ *
+ * Symbol ids are namespaced `dw-<name>` (e.g. dw-house). Consumed via the dw_icon()
+ * helper (inc/template.php) or directly: <svg class="dw-icon"><use href="#dw-house"></use></svg>.
+ *
+ * Run:  npm run icons   (also chained into `npm run build`).
+ * The kit is the curated icon set, so the sprite is exactly the icons we use (~40, ~40 KB)
+ * — replacing the 689 KB Font Awesome webfont.
+ */
+import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
+import { resolve } from 'node:path';
+
+const scope = 'node_modules/@awesome.me';
+let kit;
+try {
+  kit = readdirSync(scope).find((d) => d.startsWith('kit-'));
+} catch {
+  /* scope missing */
+}
+if (!kit) {
+  console.error('build-icons: no @awesome.me/kit-* installed — skipping (run `npm install` your FA kit first)');
+  process.exit(0);
+}
+
+const spritesDir = resolve(scope, kit, 'icons/sprites');
+const symbols = [];
+const seen = new Set();
+
+for (const file of readdirSync(spritesDir).filter((f) => f.endsWith('.svg'))) {
+  const svg = readFileSync(resolve(spritesDir, file), 'utf8');
+  for (const m of svg.matchAll(/<symbol id="([^"]+)"([\s\S]*?)<\/symbol>/g)) {
+    const name = m[1];
+    if (seen.has(name)) continue; // first style wins on a name clash
+    seen.add(name);
+    symbols.push(`<symbol id="dw-${name}"${m[2]}</symbol>`);
+  }
+}
+
+symbols.sort();
+const sprite =
+  '<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">\n' +
+  symbols.join('\n') +
+  '\n</svg>\n';
+
+writeFileSync('icons.svg', sprite);
+console.log(`build-icons: wrote icons.svg — ${symbols.length} icons from ${kit}`);

+ 9 - 0
css/styles.scss

@@ -101,3 +101,12 @@ $offcanvas-vertical-height: 100px !important;
 .table.icons {
 .table.icons {
   --bs-table-bg: transparent;
   --bs-table-bg: transparent;
 }
 }
+
+// Sprite icons (dw_icon() / <use href="#dw-…">, from the FA Pro kit → icons.svg).
+// Sized like a font icon: 1em square, inherits text color.
+.dw-icon {
+  width: 1em;
+  height: 1em;
+  fill: currentColor;
+  vertical-align: -0.125em;
+}

File diff suppressed because it is too large
+ 41 - 0
icons.svg


+ 28 - 0
inc/template.php

@@ -355,3 +355,31 @@ add_action('comment_form', 'dw_comment_button' );
 function dw_comment_button() {
 function dw_comment_button() {
     echo '<button class="btn btn-default" type="submit">' . __( 'Submit' ) . '</button>';
     echo '<button class="btn btn-default" type="submit">' . __( 'Submit' ) . '</button>';
 }
 }
+
+
+/*============================================
+		Icon sprite (Font Awesome Pro kit → icons.svg)
+==============================================*/
+
+// Inject the SVG sprite once near the end of <body> so dw_icon() / <use href="#dw-…">
+// resolve. Built by `npm run icons` (bin/build-icons.mjs) from the FA Pro kit.
+function dw_icon_sprite() {
+	static $done = false;
+	if ( $done ) { return; }
+	$done = true;
+	$sprite = get_template_directory() . '/icons.svg';
+	if ( is_readable( $sprite ) ) {
+		echo file_get_contents( $sprite ); // trusted local sprite
+	}
+}
+add_action( 'wp_footer', 'dw_icon_sprite', 5 );
+
+// Output a sprite icon: dw_icon('house') → <svg class="dw-icon"><use href="#dw-house"></use></svg>.
+// $class adds classes (sizing/color); $title gives an accessible label (else aria-hidden).
+function dw_icon( $name, $class = '', $title = '' ) {
+	$cls  = 'dw-icon' . ( $class ? ' ' . $class : '' );
+	$a11y = $title
+		? ' role="img" aria-label="' . esc_attr( $title ) . '"'
+		: ' aria-hidden="true" focusable="false"';
+	echo '<svg class="' . esc_attr( $cls ) . '"' . $a11y . '><use href="#dw-' . esc_attr( $name ) . '"></use></svg>';
+}

+ 35 - 0
package-lock.json

@@ -8,6 +8,7 @@
       "name": "daw-wp",
       "name": "daw-wp",
       "version": "0.5.0",
       "version": "0.5.0",
       "dependencies": {
       "dependencies": {
+        "@awesome.me/kit-4256b2a3b9": "^1.0.4",
         "bootstrap": "^5.3.8",
         "bootstrap": "^5.3.8",
         "fullcalendar": "^5.11.3",
         "fullcalendar": "^5.11.3",
         "jquery": "^3.6.1",
         "jquery": "^3.6.1",
@@ -97,6 +98,18 @@
         "react-dom": "^17.0.0 || ^18.0.0"
         "react-dom": "^17.0.0 || ^18.0.0"
       }
       }
     },
     },
+    "node_modules/@awesome.me/kit-4256b2a3b9": {
+      "version": "1.0.4",
+      "resolved": "https://npm.fontawesome.com/@awesome.me/kit-4256b2a3b9/-/kit-4256b2a3b9-1.0.4.tgz",
+      "integrity": "sha512-0ZPp+npGu1pPvoi2myMDN64drlKvdu0zbWUYeMXqj097rbqMJCF0jAhx6Gh37TyEPIe+6Ko/csqeZ+rt7gKSAA==",
+      "license": "UNLICENSED",
+      "dependencies": {
+        "@fortawesome/fontawesome-common-types": "^7.2.0"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
     "node_modules/@babel/code-frame": {
     "node_modules/@babel/code-frame": {
       "version": "7.29.7",
       "version": "7.29.7",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -2838,6 +2851,15 @@
         "tslib": "^2.8.0"
         "tslib": "^2.8.0"
       }
       }
     },
     },
+    "node_modules/@fortawesome/fontawesome-common-types": {
+      "version": "7.2.0",
+      "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz",
+      "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/@hapi/address": {
     "node_modules/@hapi/address": {
       "version": "5.1.1",
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
       "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
@@ -28260,6 +28282,14 @@
         "use-sync-external-store": "^1.2.0"
         "use-sync-external-store": "^1.2.0"
       }
       }
     },
     },
+    "@awesome.me/kit-4256b2a3b9": {
+      "version": "1.0.4",
+      "resolved": "https://npm.fontawesome.com/@awesome.me/kit-4256b2a3b9/-/kit-4256b2a3b9-1.0.4.tgz",
+      "integrity": "sha512-0ZPp+npGu1pPvoi2myMDN64drlKvdu0zbWUYeMXqj097rbqMJCF0jAhx6Gh37TyEPIe+6Ko/csqeZ+rt7gKSAA==",
+      "requires": {
+        "@fortawesome/fontawesome-common-types": "^7.2.0"
+      }
+    },
     "@babel/code-frame": {
     "@babel/code-frame": {
       "version": "7.29.7",
       "version": "7.29.7",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -30057,6 +30087,11 @@
         "tslib": "^2.8.0"
         "tslib": "^2.8.0"
       }
       }
     },
     },
+    "@fortawesome/fontawesome-common-types": {
+      "version": "7.2.0",
+      "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz",
+      "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw=="
+    },
     "@hapi/address": {
     "@hapi/address": {
       "version": "5.1.1",
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
       "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",

+ 3 - 1
package.json

@@ -8,7 +8,8 @@
     "url": "git@github.com:windhamdavid/daw.git"
     "url": "git@github.com:windhamdavid/daw.git"
   },
   },
   "scripts": {
   "scripts": {
-    "build": "wp-scripts build",
+    "build": "node bin/build-icons.mjs && wp-scripts build",
+    "icons": "node bin/build-icons.mjs",
     "start": "wp-scripts start"
     "start": "wp-scripts start"
   },
   },
   "devDependencies": {
   "devDependencies": {
@@ -36,6 +37,7 @@
     "webpack-cli": "^5.1.1"
     "webpack-cli": "^5.1.1"
   },
   },
   "dependencies": {
   "dependencies": {
+    "@awesome.me/kit-4256b2a3b9": "^1.0.4",
     "bootstrap": "^5.3.8",
     "bootstrap": "^5.3.8",
     "fullcalendar": "^5.11.3",
     "fullcalendar": "^5.11.3",
     "jquery": "^3.6.1",
     "jquery": "^3.6.1",

File diff suppressed because it is too large
+ 0 - 0
v4-style.min.css


Some files were not shown because too many files changed in this diff