Skip to content

Commit 495e5e8

Browse files
authored
feat(website): introduce illustration explorer (#13058)
1 parent b035dbc commit 495e5e8

10 files changed

Lines changed: 943 additions & 94 deletions

File tree

packages/website/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ src/components/Editor/ui5-autocomplete.json
1616
/icons
1717
/icons-tnt
1818
/icons-business-suite
19+
/illustrations
20+
/illustrations-tnt
1921
static/packages
2022
static/assets
2123

packages/website/build-scripts/icons-generation/index.mjs

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -131,29 +131,7 @@ const _generateIconsPage = (sourceDir, config) => {
131131

132132
const classDef = `export default function ${config.componentName}() {
133133
return (
134-
<div style={{
135-
padding: "2rem 2rem",
136-
}}>
137-
<div style={{ display: "flex", flexDirection: "column" }}>
138-
<div style={{ display: "flex", flexDirection: "column" }}>
139-
<Heading as="h2" style={{ marginBottom: "0.125rem" }}>${config.title}</Heading>
140-
<Link to="${config.npmLink}">${config.npmPackage}</Link>
141-
</div>
142-
<div style={{ marginTop: "1rem" }}>
143-
<span role="presentation"></span>
144-
<input className="icons__search" type="search" placeholder="Filter icons..." aria-label="Filter icons" onInput={function (e) {
145-
[...document.querySelectorAll("[data-icon-name]")].forEach(iconWrapper => {
146-
const iconName = iconWrapper.getAttribute("data-icon-name").toLowerCase();
147-
iconWrapper.classList.toggle("hidden", !iconName.includes(e.target.value))
148-
})
149-
150-
document
151-
.querySelector(".icon__not__found")
152-
.classList
153-
.toggle("hidden", ![...document.querySelectorAll("[data-icon-name]")].every(iconWrapper => iconWrapper.classList.contains("hidden")))
154-
}} />
155-
</div>
156-
</div>
134+
<div>
157135
<div className="icon__grid">
158136
${icons}
159137
</div>
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import { fileURLToPath } from "node:url";
4+
5+
const SAPIllustrationsConfig = {
6+
title: "SAP Illustrations",
7+
npmLink: "https://www.npmjs.com/package/@ui5/webcomponents-fiori",
8+
npmPackage: "@ui5/webcomponents-fiori",
9+
dir: "illustrations",
10+
componentName: "SAPIllustrations",
11+
subfolder: "", // No subfolder for SAP illustrations
12+
};
13+
14+
const SAPTNTIllustrationsConfig = {
15+
title: "SAP TNT Illustrations",
16+
npmLink: "https://www.npmjs.com/package/@ui5/webcomponents-fiori",
17+
npmPackage: "@ui5/webcomponents-fiori",
18+
dir: "illustrations-tnt",
19+
componentName: "SAPTNTIllustrations",
20+
subfolder: "tnt/", // TNT illustrations are in tnt/ subfolder
21+
};
22+
23+
const capitalize = (str) => {
24+
const firstLetter = str.charAt(0);
25+
const firstLetterCap = firstLetter.toUpperCase();
26+
const remainingLetters = str.slice(1);
27+
const capitalizedWord = firstLetterCap + remainingLetters;
28+
return capitalizedWord;
29+
};
30+
31+
const writeFile = (targetDir, content) => {
32+
const targetPath = path.resolve(`./${targetDir}`);
33+
const targetFile = path.resolve(`${targetPath}/index.js`);
34+
35+
if (!fs.existsSync(targetPath)) {
36+
fs.mkdirSync(targetPath, { recursive: true });
37+
}
38+
fs.writeFileSync(targetFile, content, { encoding: 'utf8', flag: 'w' });
39+
};
40+
41+
const commonImports = `
42+
import React, { useState } from 'react';
43+
import clsx from "clsx";
44+
import Heading from '@theme/Heading';
45+
import Link from '@docusaurus/Link';
46+
`;
47+
48+
const additionalImports = ``;
49+
50+
/**
51+
* Parse the IllustrationMessageType enum and extract non-deprecated illustrations
52+
*/
53+
const parseIllustrationsFromEnum = () => {
54+
const enumPath = path.join(
55+
findRoot("@ui5/webcomponents-fiori"),
56+
"src/types/IllustrationMessageType.ts"
57+
);
58+
59+
const enumContent = fs.readFileSync(enumPath, 'utf8');
60+
61+
const illustrations = [];
62+
63+
// Extract enum entries with their JSDoc comments
64+
const enumEntryRegex = /\/\*\*([\s\S]*?)\*\/\s*(\w+)\s*=\s*"(\w+)"/g;
65+
let match;
66+
67+
while ((match = enumEntryRegex.exec(enumContent)) !== null) {
68+
const [, jsdoc, enumKey, enumValue] = match;
69+
const isDeprecated = jsdoc.includes('@deprecated');
70+
71+
if (!isDeprecated) {
72+
illustrations.push({
73+
name: enumValue.startsWith('Tnt') ? enumValue.replace(/^Tnt/, '') : enumValue,
74+
displayName: enumValue, // Full enum name for display
75+
isTnt: enumValue.startsWith('Tnt')
76+
});
77+
}
78+
}
79+
80+
return illustrations;
81+
};
82+
83+
const _generateIllustrationsPage = (illustrations, config) => {
84+
let imports = ``;
85+
let illustrationCards = ``;
86+
let illustrationDataArray = ``;
87+
88+
illustrations.forEach(illustration => {
89+
const illustrationName = illustration.name;
90+
const illustrationNameImportName = `${illustrationName}`;
91+
92+
imports += `
93+
import ${illustrationNameImportName} from "${config.npmPackage}/dist/illustrations/${config.subfolder}${illustrationName}.js";
94+
import { spotSvg as ${illustrationName}SpotSvg } from "${config.npmPackage}/dist/illustrations/${config.subfolder}${illustrationName}.js";
95+
`;
96+
97+
illustrationCards += `
98+
{isVisible("${illustrationName}") && (
99+
<div
100+
className={clsx("illustration__wrapper", {
101+
"illustration__wrapper--selected": selectedIllustration?.name === "${illustrationName}"
102+
})}
103+
data-illustration-name="${illustrationName}"
104+
onClick={() => onIllustrationSelect(illustrations.find(item => item.name === "${illustrationName}"))}>
105+
106+
<div
107+
className="illustration__preview"
108+
dangerouslySetInnerHTML={{ __html: ${illustrationName}SpotSvg }}
109+
/>
110+
111+
<span className="illustration__wrapper__title">{illustrations.find(item => item.name === "${illustrationName}")?.displayName || "${illustrationName}"}</span>
112+
</div>
113+
)}`;
114+
115+
illustrationDataArray += `
116+
{ name: "${illustrationName}", displayName: "${illustration.displayName}", isTnt: ${illustration.isTnt} },`;
117+
});
118+
119+
const classDef = `export default function ${config.componentName}({ onIllustrationSelect, selectedIllustration, searchQuery = "" }) {
120+
const illustrations = [${illustrationDataArray}
121+
];
122+
123+
// Filter based on search query
124+
const filteredIllustrations = searchQuery
125+
? illustrations.filter(item => item.displayName.toLowerCase().includes(searchQuery.toLowerCase()))
126+
: illustrations;
127+
128+
// Check if each illustration should be visible
129+
const isVisible = (illustrationName) => {
130+
return filteredIllustrations.some(item => item.name === illustrationName);
131+
};
132+
133+
return (
134+
<div style={{
135+
padding: "1rem",
136+
}}>
137+
<div className="illustration__grid">
138+
${illustrationCards}
139+
</div>
140+
{filteredIllustrations.length === 0 && (
141+
<div style={{ textAlign: "center", padding: "2rem" }}>
142+
<h3>No matching illustrations found</h3>
143+
</div>
144+
)}
145+
</div>
146+
);
147+
}
148+
149+
export const illustrationsData = [${illustrationDataArray}
150+
];`;
151+
152+
return { imports, classDef };
153+
};
154+
155+
const generateIllustrationsPage = (illustrations, config) => {
156+
const { imports, classDef } = _generateIllustrationsPage(illustrations, config);
157+
158+
const content = `
159+
${commonImports}
160+
${additionalImports}
161+
${imports}
162+
${classDef}`;
163+
164+
writeFile(config.dir, content);
165+
};
166+
167+
function findRoot(pkgName) {
168+
return path.dirname(fileURLToPath(import.meta.resolve(`${pkgName}/package.json`)));
169+
}
170+
171+
// Main execution
172+
const allIllustrations = parseIllustrationsFromEnum();
173+
174+
// Split into SAP and TNT collections
175+
const sapIllustrations = allIllustrations.filter(ill => !ill.isTnt);
176+
const tntIllustrations = allIllustrations.filter(ill => ill.isTnt);
177+
178+
console.log(`Found ${sapIllustrations.length} SAP illustrations (non-deprecated)`);
179+
console.log(`Found ${tntIllustrations.length} TNT illustrations (non-deprecated)`);
180+
181+
generateIllustrationsPage(sapIllustrations, SAPIllustrationsConfig);
182+
generateIllustrationsPage(tntIllustrations, SAPTNTIllustrationsConfig);
183+
184+
console.log("Illustrations pages generated successfully!");

packages/website/docusaurus.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ const config: Config = {
163163
label: 'Icons',
164164
activeBasePath: 'icons',
165165
},
166+
{
167+
to: 'illustrations/',
168+
label: 'Illustrations',
169+
activeBasePath: 'illustrations',
170+
},
166171
{
167172
to: 'play/',
168173
label: 'Playground',

packages/website/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"generate-api-reference": "rimraf ./docs/components/fiori && rimraf ./docs/components/main && rimraf ./docs/components/compat && rimraf ./docs/components/ai && node ./build-scripts/api-reference-generation/index.mjs",
88
"generate-documentation": "rimraf ./docs/docs && node ./build-scripts/documentation-generation/index.mjs",
99
"generate-icons": "rimraf ./icons && rimraf ./icons-tnt && rimraf ./icons-business-suite && node ./build-scripts/icons-generation/index.mjs",
10-
"generate-local-env": "yarn generate-api-reference && yarn generate-documentation && yarn generate-icons",
10+
"generate-illustrations": "rimraf ./illustrations && rimraf ./illustrations-tnt && node ./build-scripts/illustrations-generation/index.mjs",
11+
"generate-local-env": "yarn generate-api-reference && yarn generate-documentation && yarn generate-icons && yarn generate-illustrations",
1112
"generate-production-env": "yarn generate-local-env && rimraf ./static/pages && rimraf ./static/assets && yarn copy:pages:compat && yarn copy:pages:ai && yarn copy:pages:fiori && yarn copy:pages:main",
1213
"docusaurus": "docusaurus",
1314
"start": "yarn generate-local-cdn && yarn generate-local-env && docusaurus start",

packages/website/src/pages/icons.css

Lines changed: 70 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,72 @@
1+
@import "./shared.css";
2+
3+
/* Main container */
4+
.icons__container {
5+
width: 100%;
6+
min-height: calc(100vh - var(--ifm-navbar-height));
7+
display: flex;
8+
flex-direction: column;
9+
}
10+
11+
/* Header with collection switcher and search */
12+
.icons__header {
13+
padding: 1rem 2rem;
14+
border-bottom: 1px solid var(--ifm-color-emphasis-300);
15+
background-color: var(--ifm-background-surface-color);
16+
display: flex;
17+
align-items: center;
18+
gap: 1.5rem;
19+
flex-wrap: wrap;
20+
}
21+
22+
/* Collection metadata (title + package link) */
23+
.icons__header__metadata {
24+
display: flex;
25+
flex-direction: column;
26+
align-items: flex-start;
27+
gap: 0.125rem;
28+
min-width: 200px;
29+
}
30+
31+
/* Collection metadata (title + package link) */
32+
.icons__header__separator {
33+
flex: 1;
34+
}
35+
36+
.icons__header__title {
37+
margin: 0;
38+
font-size: 1.25rem;
39+
font-weight: 600;
40+
color: var(--ifm-font-color-base);
41+
}
42+
43+
.icons__header__package {
44+
font-size: 0.875rem;
45+
color: var(--ifm-color-primary);
46+
text-decoration: none;
47+
}
48+
49+
.icons__header__package:hover {
50+
text-decoration: underline;
51+
}
52+
53+
/* Responsive: Stack header elements on mobile */
54+
@media (max-width: 996px) {
55+
.icons__header {
56+
flex-direction: column;
57+
align-items: flex-start;
58+
gap: 1rem;
59+
}
60+
61+
.icons__header__metadata {
62+
align-items: flex-start;
63+
width: 100%;
64+
}
65+
}
66+
167
.icon__grid {
268
display: grid;
3-
padding: 2rem 0;
69+
padding: 2rem;
470
gap: 2rem;
571
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
672
}
@@ -15,21 +81,7 @@
1581
justify-content: center;
1682
}
1783

18-
.icons__search {
19-
-webkit-appearance: none;
20-
-moz-appearance: none;
21-
appearance: none;
22-
background: var(--ifm-navbar-search-input-background-color) var(--ifm-navbar-search-input-icon) no-repeat 0.75rem center / 1rem 1rem;
23-
border: none;
24-
border-radius: 2rem;
25-
color: var(--ifm-navbar-search-input-color);
26-
cursor: text;
27-
display: inline-block;
28-
font-size: 0.9rem;
29-
height: 2.5rem;
30-
padding: 0 0.5rem 0 2.25rem;
31-
width: 12.5rem;
32-
}
84+
/* Search input styles moved to shared.css */
3385

3486
.icon__wrapper {
3587
position: relative;
@@ -81,7 +133,7 @@
81133
[data-theme='dark'] .icon__svg--picture {
82134
fill: var(--ifm-color-primary);
83135
}
84-
136+
85137

86138
.icon__wrapper__title {
87139
color: var(--ifm-font-color-base);
@@ -119,30 +171,4 @@
119171
background-color: var(--ifm-background-surface-color);
120172
}
121173

122-
123-
124-
.segmented__button {
125-
display: inline-flex;
126-
align-items: center;
127-
justify-content: center;
128-
background-color: var(--ifm-background-surface-color);
129-
padding: 0.5rem;
130-
}
131-
132-
.segmented__button__item {
133-
padding: 0.25rem 0.5rem;
134-
margin: 0 0.125rem;
135-
border-radius: 0.5rem;
136-
color: var(--ifm-navbar-link-color);
137-
}
138-
139-
.segmented__button__item:hover {
140-
cursor: pointer;
141-
transition: all 0.5s;
142-
background-color: var(--ifm-menu-color-background-active);
143-
}
144-
145-
.segmented__button__item--active {
146-
color: var(--ifm-menu-color-active);
147-
background-color: var(--ifm-menu-color-background-active);
148-
}
174+
/* Segmented button styles moved to shared.css */

0 commit comments

Comments
 (0)