Icon Replacement Guide
// Material Symbols to Phosphor Icons Mapping
// Before (Material Symbols)
<span className="material-symbols-rounded">tune</span>
<span className="material-symbols-rounded">palette</span>
<span className="material-symbols-rounded">accessibility_new</span>
<span className="material-symbols-rounded">info</span>
<span className="material-symbols-rounded">close</span>
<span className="material-symbols-rounded">save</span>
<span className="material-symbols-rounded">link</span>
<span className="material-symbols-rounded">warning</span>
<span className="material-symbols-rounded">delete</span>
<span className="material-symbols-rounded">add</span>
// After (Phosphor Icons)
import {
Gear, // tune
Palette, // palette
Accessibility, // accessibility_new
Info, // info
X, // close
FloppyDisk, // save
ShareNetwork, // link
Warning, // warning
Trash, // delete
Plus // add
} from '@phosphor-icons/react'
// Usage examples:
<Gear size={20} weight="duotone" />
<Palette size={20} weight="duotone" />
<Accessibility size={20} weight="duotone" />
<Info size={16} weight="duotone" />
<X size={20} />
<FloppyDisk size={16} />
<ShareNetwork size={16} />
<Warning size={20} weight="duotone" />
<Trash size={16} />
<Plus size={20} />
// For HTML templates (using Phosphor web icons):
<i class="ph ph-gear"></i>
<i class="ph ph-palette"></i>
<i class="ph ph-accessibility"></i>
<i class="ph ph-info"></i>
<i class="ph ph-x"></i>
<i class="ph ph-floppy-disk"></i>
<i class="ph ph-share-network"></i>
<i class="ph ph-warning"></i>
<i class="ph ph-trash"></i>
<i class="ph ph-plus"></i>
// Duotone versions for HTML:
<i class="ph-duotone ph-gear"></i>
<i class="ph-duotone ph-palette"></i>
Phosphor Usage
// Replace material-symbols-rounded with Phosphor icons
import {
Tune,
Palette,
AccessibilityNew,
Info,
Close,
Save,
Link,
X
} from '@phosphor-icons/react'
// In your JSX, replace:
// <span className="material-symbols-rounded">tune</span>
// with:
<Tune size={20} weight="duotone" />
// Example update for your Playground component:
<button
className="btn btn-sm btn-link show-control"
onClick={(e) => {
setDemoControlWindow('dialog')
}}
data-whichcontrol="component-dialog"
>
<Tune size={20} weight="duotone" />
Edit Component
</button>
<button
className="btn btn-sm btn-link show-control"
data-whichcontrol="fiddler-dialog"
onClick={(e) => {
setDemoControlWindow('css')
}}
>
<Palette size={20} weight="duotone" />
Component CSS
</button>
Updated Playground
import React, { useState, useEffect, useRef } from 'react'
import { connect } from 'react-redux'
import { store } from '../../store/index'
import {
Gear,
Palette,
AccessibilityNew,
ShareNetwork,
X,
FloppyDisk,
Trash
} from '@phosphor-icons/react'
import './Playground.css'
import PolicyAccordion from '../policy-accordion/PolicyAccordion'
// ... existing mapStateToProps and mapDispatchToProps ...
const Playground = (props) => {
// ... existing state and logic ...
return (
<div className="max-w-none mx-auto">
<div id="container-aacc1dda19" className="max-w-7xl mx-auto px-4">
<div className="w-full">
<ul className="flex border-b border-gray-200">
<li className="mr-2">
<button
className={`inline-block py-2 px-4 text-sm font-medium rounded-t-lg border-b-2 transition-colors duration-200 ${
tabWindow === 'playground'
? 'text-blue-600 border-blue-600 bg-blue-50'
: 'text-gray-500 border-transparent hover:text-gray-700 hover:border-gray-300'
}`}
onClick={(e) => {
setTabWindow('playground')
}}
>
Playground
</button>
</li>
<li className="mr-2">
<button
className={`inline-block py-2 px-4 text-sm font-medium rounded-t-lg border-b-2 transition-colors duration-200 ${
tabWindow === 'liveusage'
? 'text-blue-600 border-blue-600 bg-blue-50'
: 'text-gray-500 border-transparent hover:text-gray-700 hover:border-gray-300'
}`}
onClick={(e) => {
setTabWindow('liveusage')
getLiveUsage()
}}
>
Live Usage Report
</button>
</li>
<li className="mr-2">
<button
className={`inline-block py-2 px-4 text-sm font-medium rounded-t-lg border-b-2 transition-colors duration-200 ${
tabWindow === 'policies'
? 'text-blue-600 border-blue-600 bg-blue-50'
: 'text-gray-500 border-transparent hover:text-gray-700 hover:border-gray-300'
}`}
onClick={(e) => {
setTabWindow('policies')
}}
>
Policies
</button>
</li>
</ul>
{/* Playground Tab Content */}
<div className={`${tabWindow === 'playground' ? 'block' : 'hidden'} py-6`}>
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6 p-6">
<div className="lg:col-span-3">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Workspace Content Controls</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div>
<label htmlFor="template-select" className="block text-sm font-medium text-gray-700 mb-2">
Select a page template
</label>
<select
id="template-select"
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={selectedTemplatePath}
onChange={(e) => handleTemplateChange(e.target.value, null)}
>
{selectTemplateOptions.map((option, index) =>
<option key={index} value={option.path}>
{option.nameHint}
{option.title}
{index === 0 && "Select"}
{index !== 0 && "(" + option.name + ")"}
</option>
)}
</select>
</div>
{selectedTemplatePath && (
<div>
<label htmlFor="precontent-select" className="block text-sm font-medium text-gray-700 mb-2">
Select content for the component
</label>
<div className="flex">
<select
id="precontent-select"
className="flex-1 px-3 py-2 border border-gray-300 rounded-l-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={selectedPreContent}
onChange={(e) => handlePreContentChange(e)}
>
{selectPreContentOptions.map((option, index) =>
<option key={index} value={option.fullPagePath}>
{option.title}
{index !== 0 && "(" + option.name + ")"}
</option>
)}
</select>
<button
className="px-4 py-2 bg-blue-600 text-white rounded-r-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors duration-200"
type="button"
onClick={(e) => importContent(e)}
>
Import Content
</button>
</div>
</div>
)}
</div>
</div>
{wcmmode == 'EDIT' && selectedTemplatePath && (
<div className="lg:col-span-1">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Admin</h3>
<div className="space-y-3">
<div className="flex items-center">
<input
disabled={!fullBasePagePath}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
type="checkbox"
id="authorSwitchCheck"
onClick={(e) => {
toggleDisableMode()
}}
/>
<label className="ml-2 block text-sm text-gray-900" htmlFor="authorSwitchCheck">
{!iframeContentDisableMode && "Disable Author View"}
{iframeContentDisableMode && "Enable Author View"}
</label>
</div>
<button
className="flex items-center w-full px-3 py-2 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!fullBasePagePath || iframeContentDisableMode}
onClick={(e) => savePage(e)}
>
<FloppyDisk size={16} className="mr-2" />
Save Demo Content
</button>
{selectedPreContent && (
<button
className="flex items-center w-full px-3 py-2 text-sm text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors duration-200"
onClick={(e) => clearPage(e)}
>
<Trash size={16} className="mr-2" />
Clear Page Demo Content
</button>
)}
<button
className="flex items-center w-full px-3 py-2 text-sm text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors duration-200"
onClick={(e) => clearAllPages(e)}
>
<Trash size={16} className="mr-2" />
Clear All Demo Content
</button>
</div>
</div>
)}
</div>
<div className="border-t border-gray-200 p-6">
<div className="grid grid-cols-1 lg:grid-cols-6 gap-6">
<div className="lg:col-span-1">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Demo Controls</h3>
<div className="space-y-2">
{selectedTemplatePath && (
<>
<button
className="flex items-center w-full px-3 py-2 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors duration-200"
onClick={(e) => {
setDemoControlWindow('dialog')
}}
>
<Gear size={16} className="mr-2" />
Edit Component
</button>
<button
className="flex items-center w-full px-3 py-2 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors duration-200"
onClick={(e) => {
setDemoControlWindow('css')
}}
>
<Palette size={16} className="mr-2" />
Component CSS
</button>
<button
className="flex items-center w-full px-3 py-2 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors duration-200"
onClick={(e) => {
loadAxeCore(e)
setDemoControlWindow('axe')
}}
>
<AccessibilityNew size={16} className="mr-2" />
Axe Accessibility
</button>
</>
)}
<button
className="flex items-center w-full px-3 py-2 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors duration-200"
onClick={(e) => {
setDemoControlWindow('notes')
}}
>
<Gear size={16} className="mr-2" />
Notes
</button>
<button
className="flex items-center w-full px-3 py-2 text-sm text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-md transition-colors duration-200"
onClick={(e) => shareLink(e)}
>
<ShareNetwork size={16} className="mr-2" />
Share Link
</button>
<button
className="flex items-center w-full px-3 py-2 text-sm text-red-600 hover:text-red-800 hover:bg-red-50 rounded-md transition-colors duration-200"
onClick={(e) => clearWorkspace(e)}
>
<X size={16} className="mr-2" />
Clear Workspace
</button>
</div>
</div>
<div className="lg:col-span-5">
<iframe
id="contentIFrame"
className={`w-full border border-gray-300 rounded-lg ${
iframeContentDisableMode ? 'bg-gray-50' : 'bg-white'
} ${loadingIframes ? 'opacity-50' : 'opacity-100'} transition-opacity duration-200`}
height="800"
src={urlContentPage}
title="Component Preview"
ref={iframeContentRef}
onLoad={(iframeTarget) => {
redirectNavigation(iframeTarget)
disableAuthorPanel(iframeTarget)
setLoadedAxeCore(false)
setLoadingIframes(false)
}}
></iframe>
</div>
{/* Control Panels */}
{demoControlWindow === 'dialog' && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h3 className="text-lg font-semibold">Edit Component</h3>
<button
className="p-2 hover:bg-gray-100 rounded-md transition-colors duration-200"
onClick={(e) => setDemoControlWindow('')}
>
<X size={20} />
</button>
</div>
<iframe
id="dialogIFrame"
className="w-full h-96"
src={urlDialogComponent}
title="Component Dialog"
onLoad={(iframeTarget) => {
if (!loadingIframes) {
iframeContentRef.current.contentWindow.location.reload()
}
setLoadingIframes(false)
overrideDialog(iframeTarget)
}}
></iframe>
</div>
</div>
)}
{/* Other control panels would follow similar pattern... */}
</div>
</div>
</div>
</div>
{/* Other tab contents... */}
</div>
</div>
</div>
)
}
const ConnectedPlayground = connect(mapStateToProps, mapDispatchToProps)(Playground)
export { ConnectedPlayground as Playground }
Updated AEMComponentsDashboard
import React, { useState, useEffect } from 'react'
import { Info } from '@phosphor-icons/react'
import './AEMComponentsDashboard.css'
const AEMComponentsDashboard = (props) => {
// ... existing state logic ...
return (
<div className="dashboard">
<div className="component-controls container">
<div className="row">
<div className="col-lg-3 col-xs-6 mb-3">
<label htmlFor="filterListInput" className="form-label filter text-gray-700 font-medium">
Filter list by component name
</label>
<input
type="text"
className="form-control rounded-md border-gray-300 focus:border-blue-500 focus:ring-blue-500"
id="filterListInput"
placeholder="Search components..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
{/* ... existing group filter logic ... */}
</div>
</div>
<div className="component-list container">
{ loading && <div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">Loading...</span>
</div> }
{ aemComponentList && (
<div>
{ aemComponentList.map((group, index) => (
<div key={index} className="component-group mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-4">{group.group}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{ group.aemComponentList.map((component, index) => (
<div key={index} className="transform hover:scale-105 transition-transform duration-200">
<a
className="card block bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow duration-300 p-6"
href={
'/bin/aemplayground.getpage.html' +
component.path +
'?ccp=' +
currentComponentPath
}
title={ component.path }
>
<div className="card-body">
{ component.iconPath && (
<img
className="w-16 h-16 mx-auto mb-4 object-contain"
src={ component.iconPath }
alt={ component.name }
/>
) }
{ !component.iconPath && (
<img
className="w-16 h-16 mx-auto mb-4 object-contain opacity-50"
src="/etc.clientlibs/aem-component-catalog/clientlibs/clientlib-react/resources/placeholder.svg"
alt="Placeholder"
/>
) }
<h5 className="text-lg font-semibold text-gray-900 mb-2">{ component.title }</h5>
<h6 className="text-sm text-gray-600 mb-4">{ component.description }</h6>
<span className="inline-flex items-center text-blue-600 hover:text-blue-800 text-sm font-medium">
<Info size={16} weight="duotone" className="mr-2" />
View {component.name} Details
</span>
</div>
</a>
</div>
))}
</div>
</div>
)) }
</div>
)}
</div>
</div>
);
};
export { AEMComponentsDashboard };
Theme Context
// ThemeContext.js
import React, { createContext, useContext, useEffect, useState } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') || 'theme-helium';
});
useEffect(() => {
document.body.classList.remove('theme-helium', 'theme-dark', 'theme-light');
document.body.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = () => useContext(ThemeContext);
head.html
<template data-sly-template.head="${ @ page, pwa }" data-sly-use.headlibRenderer="headlibs.html"
data-sly-use.headResources="head.resources.html">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${ page.title } ${ page.brandSlug ? ' | ' : '' } ${ page.brandSlug }</title>
<meta data-sly-test.keywords="${ page.keywords }" name="keywords" content="${ keywords }" />
<meta data-sly-test.description="${ page.description || properties['jcr:description'] }" name="description"
content="${ description }" />
<meta data-sly-test.templateName="${ page.templateName }" name="template" content="${ templateName }" />
<meta data-sly-test="${ page.robotsTags }" name="robots" content="${ page.robotsTags @ join=', ' }">
<sly data-sly-test="${ pwa.enabled }">
<link rel="manifest" href="${ pwa.manifestPath }" crossorigin="use-credentials" />
<meta name="theme-color" content="${ pwa.themeColor }" />
<link rel="apple-touch-icon" href="${ pwa.iconPath }" />
<sly data-sly-use.clientlib="core/wcm/components/commons/v1/templates/clientlib.html"
data-sly-call="${ clientlib.css @ categories='core.wcm.components.page.v2.pwa' }"></sly>
<meta name="cq:sw_path" content="${ pwa.serviceWorkerPath @ context ='text' }" />
</sly>
<sly data-sly-include="head.links.html"></sly>
<sly data-sly-include="customheaderlibs.html"></sly>
<sly data-sly-call="${
headlibRenderer.headlibs @
page = page,
designPath = page.designPath,
staticDesignPath = page.staticDesignPath,
clientLibCategories = page.clientLibCategories,
clientLibCategoriesJsHead = page.clientLibCategoriesJsHead,
hasCloudconfigSupport = page.hasCloudconfigSupport
}"></sly>
<sly data-sly-test.appResourcesPath=${page.appResourcesPath}
data-sly-call="${ headResources.favicons @ path = appResourcesPath }"></sly>
<sly data-sly-list="${ page.htmlPageItems }">
<script data-sly-test="${ item.location.name == 'header' }"
data-sly-element="${ item.element.name @ context='unsafe' }" data-sly-attribute="${ item.attributes }"></script>
</sly>
<!-- Tailwind CSS v4 -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss"> @theme {
/* Brand Colors */
--color-abbvie: rgb(7, 29, 73);
--color-abbvie-dark: rgb(5, 21, 55);
--color-abbvie-light: rgb(15, 45, 95);
/* Typography */
--font-family-brand: "Adobe Clean", "Helvetica Neue", Arial, sans-serif;
/* Layout */
--max-width-container: 1200px;
--spacing-navbar: 2rem;
/* Shadows */
--shadow-navbar: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-dropdown: 0 4px 6px rgba(0, 0, 0, 0.1);
/* Transitions */
--duration-fast: 150ms;
--duration-normal: 250ms;
}
/* Custom utility classes for AEM components */
@utility navbar {
@apply bg-abbvie shadow-navbar;
}
@utility navbar-brand {
@apply flex items-center gap-2 text-white no-underline hover:text-gray-200 transition-colors duration-fast;
}
@utility nav-link {
@apply text-white hover:text-gray-200 no-underline px-3 py-2 rounded-md transition-colors duration-fast;
}
@utility nav-link-active {
@apply bg-abbvie-light;
}
@utility dropdown-menu {
@apply absolute top-full left-0 mt-1 bg-white shadow-dropdown rounded-md py-1 z-50 min-w-48;
}
@utility dropdown-item {
@apply block px-4 py-2 text-gray-700 hover:bg-gray-100 no-underline transition-colors duration-fast;
}
@utility footer-brand {
@apply bg-abbvie text-white;
}
</style>
<!-- Phosphor Icons (replacing Material Symbols) -->
<script src="https://unpkg.com/@phosphor-icons/web@2.1.1"></script>
</template>
body.html
<sly data-sly-use.pageHeader="com.xpediantsolutions.ccatalog.core.models.PageHeader">
<header class="navbar px-8 py-4 sticky top-0 z-40">
<nav class="max-w-7xl mx-auto w-full">
<div class="flex items-center justify-between">
<!-- Brand -->
<a class="navbar-brand" href="/">
<img
src="/etc.clientlibs/aem-component-catalog/clientlibs/clientlib-react/resources/images/logos/logo-nova.svg"
alt="NOVA Logo" class="h-8 w-auto">
<span class="font-bold text-lg">Component Catalog</span>
</a>
<!-- Mobile menu button -->
<button
class="lg:hidden inline-flex items-center justify-center p-2 rounded-md text-white hover:text-gray-200 hover:bg-abbvie-light focus:outline-none focus:ring-2 focus:ring-white"
type="button" x-data="{ open: false }" @click="open = !open" :aria-expanded="open"
aria-label="Toggle navigation">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path x-show="!open" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16" />
<path x-show="open" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<!-- Desktop Navigation -->
<div class="hidden lg:flex lg:items-center lg:space-x-1">
<ul class="flex space-x-1">
<!-- Link 1 -->
<sly data-sly-test.link1Dropdown="${ !pageHeader.headerMainLink1Url }">
<li class="relative" x-data="{ open: false }">
<button class="nav-link ${ pageHeader.activateMainLink1 ? 'nav-link-active' : '' } flex items-center"
@click="open = !open" @keydown.escape="open = false" :aria-expanded="open"> ${
pageHeader.headerMainLink1Label } <i class="ph ph-caret-down ml-1"
:class="{ 'rotate-180': open }"></i>
</button>
<ul class="dropdown-menu" x-show="open" @click.away="open = false" x-transition
data-sly-list="${ pageHeader.headerSublinks1List }">
<li>
<a class="dropdown-item" href="${ item.subUrl }"> ${ item.subLabel } </a>
</li>
</ul>
</li>
</sly>
<sly data-sly-test="${ !link1Dropdown }">
<li>
<a class="nav-link ${ pageHeader.activateMainLink1 ? 'nav-link-active' : '' }"
href="${ pageHeader.headerMainLink1Url }"> ${ pageHeader.headerMainLink1Label } </a>
</li>
</sly>
<!-- Link 2 -->
<sly data-sly-test.link2Dropdown="${ !pageHeader.headerMainLink2Url }">
<li class="relative" x-data="{ open: false }">
<button class="nav-link ${ pageHeader.activateMainLink2 ? 'nav-link-active' : '' } flex items-center"
@click="open = !open" @keydown.escape="open = false" :aria-expanded="open"> ${
pageHeader.headerMainLink2Label } <i class="ph ph-caret-down ml-1"
:class="{ 'rotate-180': open }"></i>
</button>
<ul class="dropdown-menu" x-show="open" @click.away="open = false" x-transition
data-sly-list="${ pageHeader.headerSublinks2List }">
<li>
<a class="dropdown-item" href="${ item.subUrl }"> ${ item.subLabel } </a>
</li>
</ul>
</li>
</sly>
<sly data-sly-test="${ !link2Dropdown }">
<li>
<a class="nav-link ${ pageHeader.activateMainLink2 ? 'nav-link-active' : '' }"
href="${ pageHeader.headerMainLink2Url }"> ${ pageHeader.headerMainLink2Label } </a>
</li>
</sly>
</ul>
</div>
</div>
<!-- Mobile Navigation -->
<div class="lg:hidden mt-4" x-show="open" x-transition>
<ul class="space-y-2">
<!-- Mobile Link 1 -->
<sly data-sly-test.link1Dropdown="${ !pageHeader.headerMainLink1Url }">
<li x-data="{ open: false }">
<button
class="nav-link w-full text-left ${ pageHeader.activateMainLink1 ? 'nav-link-active' : '' } flex items-center justify-between"
@click="open = !open"> ${ pageHeader.headerMainLink1Label } <i class="ph ph-caret-down"
:class="{ 'rotate-180': open }"></i>
</button>
<ul class="mt-2 ml-4 space-y-1" x-show="open" x-transition
data-sly-list="${ pageHeader.headerSublinks1List }">
<li>
<a class="nav-link text-sm" href="${ item.subUrl }"> ${ item.subLabel } </a>
</li>
</ul>
</li>
</sly>
<sly data-sly-test="${ !link1Dropdown }">
<li>
<a class="nav-link w-full ${ pageHeader.activateMainLink1 ? 'nav-link-active' : '' }"
href="${ pageHeader.headerMainLink1Url }"> ${ pageHeader.headerMainLink1Label } </a>
</li>
</sly>
<!-- Mobile Link 2 -->
<sly data-sly-test.link2Dropdown="${ !pageHeader.headerMainLink2Url }">
<li x-data="{ open: false }">
<button
class="nav-link w-full text-left ${ pageHeader.activateMainLink2 ? 'nav-link-active' : '' } flex items-center justify-between"
@click="open = !open"> ${ pageHeader.headerMainLink2Label } <i class="ph ph-caret-down"
:class="{ 'rotate-180': open }"></i>
</button>
<ul class="mt-2 ml-4 space-y-1" x-show="open" x-transition
data-sly-list="${ pageHeader.headerSublinks2List }">
<li>
<a class="nav-link text-sm" href="${ item.subUrl }"> ${ item.subLabel } </a>
</li>
</ul>
</li>
</sly>
<sly data-sly-test="${ !link2Dropdown }">
<li>
<a class="nav-link w-full ${ pageHeader.activateMainLink2 ? 'nav-link-active' : '' }"
href="${ pageHeader.headerMainLink2Url }"> ${ pageHeader.headerMainLink2Label } </a>
</li>
</sly>
</ul>
</div>
</nav>
</header>
</sly>
<main class="mt-8 min-h-screen">
<sly data-sly-use.templatedContainer="com.day.cq.wcm.foundation.TemplatedContainer"
data-sly-repeat.child="${ templatedContainer.structureResources }"
data-sly-resource="${ child.path @ resourceType=child.resourceType, decorationTagName='div' }"></sly>
</main>
<div class="footer-brand">
<footer class="max-w-7xl mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-center">
<div class="flex justify-center md:justify-start">
<img
src="/etc.clientlibs/aem-component-catalog/clientlibs/clientlib-react/resources/images/logos/logo-abbvie.svg"
alt="AbbVie Logo" class="h-12 w-auto">
</div>
<div class="flex justify-center md:justify-end">
<div class="text-center md:text-right">
<p class="text-sm">
<a href="https://abbv.ie/nova" target="_blank"
class="text-white hover:text-gray-200 transition-colors duration-fast no-underline">abbv.ie/nova</a>
</p>
</div>
</div>
</div>
</footer>
</div>
<!-- Alpine.js for dropdown functionality -->
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
`vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
build: {
outDir: 'build',
rollupOptions: {
input: {
main: path.resolve(__dirname, 'src/index.js')
},
output: {
// Maintain consistent naming for AEM clientlib generation
entryFileNames: 'static/js/[name].[hash].js',
chunkFileNames: 'static/js/[name].[hash].js',
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return 'static/css/[name].[hash].css'
}
return 'static/media/[name].[hash].[ext]'
},
// Generate manifest for clientlib processing
manifest: true
}
},
// Generate asset manifest for AEM integration
manifest: true
},
server: {
proxy: {
// Proxy AEM requests during development
'/bin': 'http://localhost:4502',
'/libs': 'http://localhost:4502',
'/etc.clientlibs': 'http://localhost:4502',
'/content': 'http://localhost:4502'
}
},
css: {
postcss: './postcss.config.js'
}
})
vite.clientlib.js
const fs = require('fs');
const path = require('path');
const clientlibGenerator = require('aem-clientlib-generator');
const BUILD_DIR = path.join(__dirname, '..', 'build');
const CLIENTLIB_DIR = path.join(
__dirname,
'..',
'..',
'ui.apps',
'src',
'main',
'content',
'jcr_root',
'apps',
'aem-component-catalog',
'clientlibs'
);
/**
* Extract entrypoint files from Vite's manifest.json
*/
function getViteEntrypoints() {
const manifestPath = path.join(BUILD_DIR, '.vite', 'manifest.json');
if (!fs.existsSync(manifestPath)) {
throw new Error(`Vite manifest not found at ${manifestPath}`);
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const entrypoints = [];
// Find the main entry point
Object.entries(manifest).forEach(([key, value]) => {
if (value.isEntry) {
// Add the main JS file
if (value.file) {
entrypoints.push(value.file);
}
// Add associated CSS files
if (value.css) {
entrypoints.push(...value.css);
}
}
});
return entrypoints;
}
/**
* Generate AEM ClientLib
*/
async function buildClientLib() {
try {
const entrypoints = getViteEntrypoints();
console.log('Found entrypoints:', entrypoints);
const clientlibConfig = {
context: BUILD_DIR,
clientLibRoot: CLIENTLIB_DIR,
libs: {
name: 'clientlib-react',
allowProxy: true,
categories: ['aem-component-catalog.react'],
serializationFormat: 'xml',
cssProcessor: ['default:none', 'min:none'],
jsProcessor: ['default:none', 'min:none'],
assets: {
js: entrypoints.filter(fileName => fileName.endsWith('.js')),
css: entrypoints.filter(fileName => fileName.endsWith('.css')),
resources: {
cwd: '.',
files: ['**/*.*'],
flatten: false,
ignore: entrypoints
}
}
}
};
await clientlibGenerator(clientlibConfig);
console.log('ClientLib generated successfully!');
} catch (error) {
console.error('Error building clientlib:', error);
process.exit(1);
}
}
buildClientLib();
clientlib.config.js
const path = require('path');
const getEntrypoints = require('./utils/entrypoints');
const BUILD_DIR = path.join(__dirname, 'build');
const CLIENTLIB_DIR = path.join(
__dirname,
'..',
'ui.apps',
'src',
'main',
'content',
'jcr_root',
'apps',
'aem-component-catalog',
'clientlibs'
);
const ASSET_MANIFEST_PATH = path.join(BUILD_DIR, 'asset-manifest.json');
const entrypoints = getEntrypoints(ASSET_MANIFEST_PATH);
// Config for `aem-clientlib-generator`
module.exports = {
context: BUILD_DIR,
clientLibRoot: CLIENTLIB_DIR,
libs: {
name: 'clientlib-react',
allowProxy: true,
categories: ['aem-component-catalog.react'],
serializationFormat: 'xml',
cssProcessor: ['default:none', 'min:none'],
jsProcessor: ['default:none', 'min:none'],
assets: {
// Copy entrypoint scripts and stylesheets into the respective ClientLib
// directories (in the order they are in the entrypoints arrays). They
// will be bundled by AEM and requested from the HTML. The remaining
// chunks (placed in `resources`) will be loaded dynamically
js: entrypoints.filter(fileName => fileName.endsWith('.js')),
css: entrypoints.filter(fileName => fileName.endsWith('.css')),
// Copy all other files into the `resources` ClientLib directory
resources: {
cwd: '.',
files: ['**/*.*'],
flatten: false,
ignore: entrypoints
}
}
}
};
That makes this sort of our store front for marketing NOVA both internally and to creative agencies that will end up working with NOVA, so usability and presentation are important aspects.
Another aspect that Shivendra also mentioned is for websites, a large majority of this that exists today for the ivy(websites) channel is AEM authored pages that have been a pain point to keep updated. With that in mind we are hoping to bring the content of this down to the developers that know the most about this information and code manage it, so it stays most up to date.
We are not looking for all of this now, but wanted to paint some more context to this project so we can keep it in mind in case it drives some early decisions on how this is built.
End state goal (future)
Single location for all things NOVA documentations. Driven from repository code/files not AEM Authoring
General
- Describe what NOVA is how it is used and why it is important (elevator pitch)
- Link list to Branded Design Systems TBD on format but a central/shareable place where we can refer brands and agencies to their branded figma design system.
- Release Notes
Components
- Playground – able to see and understand what our component can be authored as including the selectable options in our dialogs.
- Detailed descriptions - its uses, and misuses
- Branded View - Toggle through existing branded components
- Tokens and variable CSS - Central documentation that drives the OTB component and its styling to its Branded version
- Analytics – highlight analytics that are auto generated alongside what is authorable and able to be added or modified.
- Nice to have – Component use count
Templates
- Catalog - List of existing templates filtered by brands + BU
- Nice to have – Template use count
1. Code Project Structure
The project follows the standard AEM maven archetype.
Key Subprojects
- ui.apps - UI AEM components
- listaemcomponents-react - anchor for react code
- listaemtemplates-react - anchor for react code
- playground-react - anchor for react code
- core - Java AEM components and Servlets
- ui.frontend – builds css based on boostrap
- ui.frontend-react – react code for playground, listaemcomponents and listaemtemplates JavaScript components.

Image Placeholder: Diagram of the project structure might appear here in the original PDF
2. Installation Instructions
- Build project:
mvn clean install - Install package located under "all/target" -
aem-component-catalog.all-1.0.0-SNAPSHOT.zip - Create 2 pages
- On the first page "List AEM Components" on "Component" tab, configure it with the fields requested.

Image Placeholder: Screenshot of the Component tab configuration screen
- On the second page "List AEM Templates" on "Template" tab, configure it with the fields requested.

Image Placeholder: Screenshot of the Template tab configuration screen
- Edit the "Header" properties in order to point to the created previous pages.

Image Placeholder: Screenshot of Header properties configuration
3. Configurations
3.1 OSGi Configurations
a. AEM Component Catalog Configuration Service
com.xpediantsolutions.ccatalog.core.services.impl.ConfigurationServiceImpl
General configurations
- Playground Base Path: The temporary path where all pages and workspaces are created
- Playground Template Path: The template used for the playground
- Playground Template Component Path: The path where the playground component is inserted in the generated page
- Template Content Path: A fallback configuration that maps the path of the component to be examined in the generated page. This setting can also be configured in the AEM component.
- Max Number of Workspaces: The maximum number of workspaces that can be created
- Workspace Cleanup Time (milliseconds): The lifespan of workspaces. Workspaces are automatically deleted after this time.

Image Placeholder: Screenshot of the Configuration Service settings
b. AEM Component Catalog Dialog Content Command
com.xpediantsolutions.ccatalog.core.services.impl.AEMDialogProxyCommandImpl
Replaces the Dialog code with custom code using a placeholder, use to apply workarounds for components JavaScript.
c. AEM Component Catalog Dialog Proxy
com.xpediantsolutions.ccatalog.core.servlets.AEMDialogProxy
Adds custom JavaScript for all dialogs. Dialogs are served through a proxy which allows to add custom JavaScript.
3.2 Component Configurations
a. listaemcomponents (List AEM Components)
Components Tab
- Groups to include: Components groups to include
- Paths to include (Must match both groups and Paths): Components paths to include.

Image Placeholder: Screenshot of the Components Tab configuration
Templates Tab
- Template Title matching String: Regular expression that filters the path of the template
- Template Configuration Path Pattern: Regular expression that filters the path of the template
Both values do the same thing, but is meant for the regular expression to be simpler since its split into two. Example: /conf/xpomnichannel/settings(.)*

Image Placeholder: Screenshot of the Templates Tab configuration
Live Usage Tab
- Live Usage Query Path: Path to limit the live usage query. If empty it uses
/content

Image Placeholder: Screenshot of the Live Usage Tab configuration
3.3 listaemtemplates (List AEM Templates)
Templates Tab
- Template Title matching String: Regular expression that filters the path of the template
- Template Configuration Path Pattern: Regular expression that filters the path of the template
Both values do the same thing, but is meant for the regular expression to be simpler, since its split into two. Example: /conf/xpomnichannel/settings(.)*
Live Usage Tab
- Live Usage Query Path (ex /content/…): Path to limit the live usage query. If empty it uses
/content
4. Permissions on Author and Publish
4.1. Author
a. Author (author permissions)

Image Placeholder: Screenshot of author permissions configuration
b. adaptive-write system user
- read
/apps/conf/content/libs
- read, create, update, delete
/var/xpediant
4.2. Publish
a. Anonymous
- read
/libs/clientlibs/libs/cq/apps/xpomnichannel(Need permission to the dialogs)/conf/xpomnichannel
- read, create, update, delete
/var/xpediant
b. adaptive-write system user
- read
/apps/conf/content/libs
- read, create, update, delete
/var/xpediant
5. Template list overview
A list of templates will be displayed. Users can search by title, and clicking on a template will show its live usage.

Image Placeholder: Screenshot of the template list interface
6. Component list overview
A list of components will be displayed. Users can search by title or by group, and clicking component will be redirect to the component playground page.

Image Placeholder: Screenshot of the component list interface
7. Playground Pre-content authoring overview
- Click the "Enable Author View"

Image Placeholder: Screenshot showing the "Enable Author View" button
- The page will change to author mode, author the component and then click on "SAVE DEMO CONTENT".

Image Placeholder: Screenshot of the author mode interface
- A new page will be added to the "import content" dropdown.
- To delete a demo page, click on "CLEAR PAGE DEMO CONTENT", the page will be removed from the "import content" dropdown.

Image Placeholder: Screenshot of the import content dropdown
- Click on "Enable Publish View" again, to return to the Publish View.

Image Placeholder: Screenshot showing the publish view interface
8. Workspaces
Each user gets their own workspace, and a cookie named: XP_COMP_CTLG_ID is generated to track the workspace identifier. The workspace is automatically deleted after a configurable amount of time.
Alternatively, clicking on "Clear Workspace" will delete the current user's workspace.
Users can share their workspace by appending the workspace identifier to the WS URL parameter. This parameter is displayed when clicking the 'SHARE LINK' button.

Image Placeholder: Screenshot showing the share link functionality
Additionally, there is a maximum number of workspaces that can be created, which is controlled by an OSGi configuration.
9. Features
- Hide dialog fields: Fields on the dialog in the playground may be hidden if they have the class (hide-in-catalog). The class is added in the dialog with granite: class attribute. The field is hidden through a custom js script in the OSGI config.

> **Image Placeholder**: *Example of hidden dialog fields*
- **Component Thumbnails**: The image is source from the component UI folder. The "icon.png" file will be used.

> **Image Placeholder**: *Example of component thumbnails*
- **Template Thumbnails**: The image is source from the template UI folder. The "thumnail.png" file will be used.

> **Image Placeholder**: *Example of template thumbnails*
## 10. User Flow
### Components
- The user navigates to the component page.

> **Image Placeholder**: *Screenshot of the component page*
- The user clicks on a specific component, and the playground page for that component is displayed.

> **Image Placeholder**: *Screenshot of the playground page*
- The user selects a template and enables the playground options.

> **Image Placeholder**: *Screenshot of the playground options*
### Templates
- The user navigates to the template page.

> **Image Placeholder**: *Screenshot of the template page*
- The user clicks on a specific template and the usage report is displayed.

> **Image Placeholder**: *Screenshot of the usage report*
## 11. Author Vs Publish experience
The Admin section is not available in the publish instance, so pre-generated content pages cannot be created there. These pages must be created and managed in the author instance.
### Author (Author instance capabilities)

> **Image Placeholder**: *Screenshot of the author instance interface*
### Publish (Publish instance capabilities)

> **Image Placeholder**: *Screenshot of the publish instance interface*
---
## Jan 28th
> About 10 days ago, I received a response.
>
> I have not yet received a follow-up on the design resources or access to the development environment related to this follow-up.
>
> I am sending a follow-up communication to check the status and address or note the following.
>
> - Establish a regular cadence for project updates. It can be recurring weekly team meetings or regular informal check-ins.
> - We can collaborate as needed.
> - Where is this project? What is its status? Can you walk me through the project plan?
> - My manager is nervous. During bi-weekly 1-on-1 meetings, he wants me to have a better grasp.
> - What’s left to be completed?
> - Are there any roadblocks?
> - Can you send a weekly update?
> - When are we targeting the final delivery of the component catalog?
>
> Please help me to draft an appropriate communication. Keep the tone friendly and casual.
Hi Team,
I hope this message finds you well. First, I want to thank you for the fantastic progress you’ve made on the component catalog. The functionality demonstrated last week was impressive, and it’s clear that a lot of effort went into it. Additionally, I noticed significant improvements in the design since the previous demo—excellent work on that front!
As we move forward, I’d like to ensure that we maintain our focus on refining the design. I understand that the design options may be somewhat constrained by the application’s templates, but even small refinements can greatly enhance the overall experience. To help us stay aligned, I’d like to request the following:
Design Resources
- If you have a Figma file or any other design references, could you please share them with me?
- If no design file is currently available, access to the development environment would be needed so I can begin reviewing the design direction.
Purpose
- Ensure we meet expectations for the catalog’s visual appeal, adding that extra layer of polish for our end users as without that we would have only met 65% of our goal and for a fact adoption will be severely impacted.
- Provide early feedback to avoid last-minute design adjustments.
I know this is still a work in progress, and the strides you’ve made so far are a testament to your dedication. With a bit more collaboration on the visual details, I’m confident we’ll deliver something truly outstanding.
Let me know how I can best support you or if there’s a time we can discuss the next steps. Thanks again for all your great work so far—I truly appreciate everything you’re doing!
Best regards,
---
Their reply,
Hi @Tolbert, Victor
Thank you for your kind words and thoughtful feedback. I’m glad to hear the progress and improvements resonated with you.
Our senior designer is currently reviewing your input to address your points effectively. We’ll share a timeline for providing the necessary design resources or access when they will be ready for review. Additionally, we’ll arrange a call to discuss the next steps once the designs are finalized.
Thank you again for your collaboration!
Thanks,
Bala Rengaswamy
XPEDIANT DIGITAL
CELL 513.289.7678
EMAIL brenga@xpediantsolutions.com
www.xpediantdigital.com
From: Tolbert, Victor <victor.tolbert@abbvie.com> Sent: Thursday, January 16, 2025 10:31 AMTo: Rengaswamy, Bala <bala.rengaswamy@abbvie.com>; Krovvidi, Chinmay <chinmay.krovvidi@abbvie.com>; Bala Rengaswamy <brenga@xpediantsolutions.com>; Chinmay Krovvidi <ckrovvidi@xpediantsolutions.com>Cc: Singh, Shivendra Vikram <shivendravikram.singh@abbvie.com>; Dalvi, Amol N <amol.dalvi@abbvie.com>; Ludolph, Jacob <jacob.ludolph@abbvie.com>; qusai@xpediantsolutions.comSubject: Request for Design Resources and Next Steps for Component Catalog
When user lands on the component playground page, he will:
- Choose the options from the component's dialog (rendering can be customizable). and save to render the component.
- Choose a brand template and validate how the chosen options for the component render with the applicable brand policies(Styles/Theme).
- Test and play around with the HTML of the component by updating the css rules before the actual implementation
- Tap into the reporting to look at the usage of the component.
- Add Relevant documentation or comments related to component.
- Save his changes and come back later to continue. Reset his component playground.
How It Works?
- Catalog Home Page is configured with AEM apps and templates path to populate the list of components by group.
- Component playground page is a single page using a specific template. This page loads below options:
- Selectable templates
- Component dialog options
- HTML Source fiddler
- Styles and policies available
- Link to show usages of component
- Text box to Save Comments
- The component playground page is tied to logged in user and user can saye or reset changes. Admin can control and clear user data.
- The Visibility of dialog fields rendered on the component playground page can be controlled via configurations on the actual component's dialog xml.
- The catalog templates and services are independent of other AEM template implementations. It uses the current version of the dialogs and configured policies and configurations for the selected template and component.
- Additional information such as styles and policies are loaded dynamically based on the template selection and made available for user selection.