feat: implement @repo/brand package for centralized asset management and automated distribution to consuming apps

This commit is contained in:
Firman Ramdhani
2026-07-29 12:27:27 +07:00
parent 6c67fbc68e
commit fcea8f221b
15 changed files with 162 additions and 10 deletions
+3
View File
@@ -33,6 +33,9 @@ dist-ssr
server/dist
public/dist
# Auto-generated brand assets (copied from @repo/brand)
**/public/brand/
# Storybook Build Outputs
*storybook.log
storybook-static
+5 -2
View File
@@ -2,10 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/brand/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Eigen — Company Profile</title>
<meta name="description" content="Eigen — Enterprise-grade solutions for modern businesses. Discover our products, services, and vision." />
<meta
name="description"
content="Eigen — Enterprise-grade solutions for modern businesses. Discover our products, services, and vision."
/>
</head>
<body>
<div id="app"></div>
+4 -2
View File
@@ -4,13 +4,15 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false",
"build": "tsc && vite build",
"copy:brand": "node ../../packages/brand/scripts/copy-brand-assets.js",
"dev": "pnpm run copy:brand && vite --clearScreen false",
"build": "pnpm run copy:brand && tsc && vite build",
"preview": "vite preview",
"lint": "eslint \"src/**/*.ts\"",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/brand": "workspace:*",
"@repo/core-api": "workspace:*",
"@repo/core-i18n": "workspace:*",
"@repo/core-storage": "workspace:*",
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en" translate="no">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/brand/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="google" content="notranslate" />
<title>Applications</title>
+4 -2
View File
@@ -4,8 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false",
"build": "tsc && vite build",
"copy:brand": "node ../../packages/brand/scripts/copy-brand-assets.js",
"dev": "pnpm run copy:brand && vite --clearScreen false",
"build": "pnpm run copy:brand && tsc && vite build",
"preview": "vite preview",
"lint": "eslint \"src/**/*.ts\"",
"test": "vitest run",
@@ -14,6 +15,7 @@
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@repo/brand": "workspace:*",
"@repo/core-api": "workspace:*",
"@repo/core-events": "workspace:*",
"@repo/core-i18n": "workspace:*",
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+32
View File
@@ -0,0 +1,32 @@
# @repo/brand
This package serves as the single source of truth for our brand identity, containing static visual assets (logos, favicons) and brand information.
## 📁 Public Assets
Place your raw, static brand assets inside the `public/` directory:
- **`logo.png`** — Primary brand logo
- **`favicon.svg`** — Browser favicon
### How Assets are Distributed
Due to monorepo constraints, frameworks like Vite or Next.js do not natively serve static assets from external packages. To solve this, these files are automatically copied into the consuming apps' `public/brand/` directories via the `scripts/copy-brand-assets.js` script.
This makes them directly available in the browser at:
- `/brand/logo.png`
- `/brand/favicon.svg`
---
## 🚀 Usage in Consuming Apps
### 1. Automate the Copy Process
To ensure the consuming app always has the latest brand assets, add the copy script to the app's `package.json` (e.g., in `apps/web/package.json`):
```json
"scripts": {
"copy:brand": "node ../../packages/brand/scripts/copy-brand-assets.js",
"dev": "pnpm run copy:brand && vite --clearScreen false",
"build": "pnpm run copy:brand && tsc && vite build"
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@repo/brand",
"version": "0.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./public/*": "./public/*"
},
"license": "MIT",
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"typescript": "5.5.4"
}
}
+6
View File
@@ -0,0 +1,6 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Blok Utama (Brand Color) -->
<rect x="6" y="6" width="16" height="16" rx="4" fill="#652ed9"/>
<!-- Blok Sekunder (Memberikan kesan struktur & kedalaman) -->
<rect x="18" y="18" width="16" height="16" rx="4" fill="#652ed9" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 359 B

@@ -0,0 +1,31 @@
const fs = require('node:fs');
const path = require('node:path');
// Resolve the source directory (packages/brand/public)
const brandPublicDir = path.resolve(__dirname, '../public');
// Resolve the destination directory (public/brand folder in the calling app)
// process.cwd() points to the app folder (e.g., apps/web) when the script is executed
const destDir = path.resolve(process.cwd(), 'public', 'brand');
// Ensure the destination folder exists
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
// Copy files using fs.cpSync (Available in Node.js 16.7+)
try {
fs.cpSync(brandPublicDir, destDir, {
recursive: true,
force: true, // Overwrite files if they already exist
filter: (src) => {
const filename = path.basename(src);
// Exclude unwanted files
return !['.gitkeep', 'README.md'].includes(filename);
},
});
console.log(`✓ Brand assets successfully copied to ${destDir}`);
} catch (error) {
console.error(`❌ Failed to copy brand assets:`, error.message);
process.exit(1);
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "My Brand",
"description": "A short description of your brand or product.",
"tagline": "Building the future, one pixel at a time.",
"socialLinks": {
"twitter": "https://twitter.com/mybrand",
"github": "https://github.com/mybrand",
"website": "https://mybrand.com"
}
}
+3
View File
@@ -0,0 +1,3 @@
import brandInfo from './data/info.json';
export type BrandInfo = typeof brandInfo;
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "@repo/typescript-config/library.json",
"include": ["."],
"exclude": ["node_modules"]
}
+41 -1
View File
@@ -88,6 +88,9 @@ importers:
apps/landing:
dependencies:
'@repo/brand':
specifier: workspace:*
version: link:../../packages/brand
'@repo/core-api':
specifier: workspace:*
version: link:../../packages/core-api
@@ -243,6 +246,9 @@ importers:
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.4.0(react-hook-form@7.79.0)
'@repo/brand':
specifier: workspace:*
version: link:../../packages/brand
'@repo/core-api':
specifier: workspace:*
version: link:../../packages/core-api
@@ -329,6 +335,15 @@ importers:
specifier: ^4.0.17
version: 4.0.17(jsdom@26.1.0)
packages/brand:
devDependencies:
'@repo/typescript-config':
specifier: workspace:*
version: link:../configs/typescript
typescript:
specifier: 5.5.4
version: 5.5.4
packages/configs/eslint:
dependencies:
'@typescript-eslint/eslint-plugin':
@@ -7444,6 +7459,18 @@ packages:
dependencies:
picomatch: 4.0.3
/fdir@6.5.0(picomatch@4.0.5):
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
dependencies:
picomatch: 4.0.5
dev: false
/fetch-cookie@2.2.0:
resolution: {integrity: sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==}
dependencies:
@@ -9989,6 +10016,11 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
/picomatch@4.0.5:
resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
engines: {node: '>=12'}
dev: false
/plist@3.1.0:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
@@ -11252,7 +11284,7 @@ packages:
is-plain-obj: 4.1.0
semver: 7.7.1
sort-object-keys: 2.0.1
tinyglobby: 0.2.15
tinyglobby: 0.2.17
dev: false
/source-map-js@1.2.1:
@@ -11627,6 +11659,14 @@ packages:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
/tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
dependencies:
fdir: 6.5.0(picomatch@4.0.5)
picomatch: 4.0.5
dev: false
/tinyrainbow@3.0.3:
resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
engines: {node: '>=14.0.0'}