Refactor imports, update dependencies, add Jenkinsfile
Some checks failed
armco-org/icon-spot/pipeline/head There was a failure building this commit

This commit is contained in:
2026-01-03 20:43:59 +05:30
commit 0f03c73068
39 changed files with 10334 additions and 0 deletions

44
.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Dependencies
node_modules
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Swap the comments on the following lines if you don't wish to use zero-installs
# Documentation here: https://yarnpkg.com/features/zero-installs
!.yarn/cache
#.pnp.*
# Testing
coverage
# Production
build
# Miscellaneous
*.local
.DS_Store
.env
Clusters
helper
IconController
IconEditor
IconInfo
Icons
IconsMerge
IconStyleSelector
IconTile
IconsSlice
IconInfoSlice

3
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,3 @@
@Library('jenkins-shared@main') _
kanikoPipeline(repoName: 'icon-spot', branch: env.BRANCH_NAME ?: 'main')

1
README.md Normal file
View File

@@ -0,0 +1 @@
# Armco Template for the tech stack: React, TS, Dart Sass, Redux Tookkit, react-redux, react browser routing, TS based plop generator

36
build-tools/build.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Get the directory of the current script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default values
DEV_FLAG=""
# Parse arguments
for arg in "$@"
do
case $arg in
--dev)
DEV_FLAG="--dev"
shift # Remove --dev from processing
;;
esac
done
echo "[BUILD:SH] Dev flag is: $DEV_FLAG"
echo "[BUILD:SH] Removing build if exists"
rm -rf build
echo "[BUILD:SH] Checking TS Types"
npx tsc
echo "[BUILD:SH] Initiating build..."
# Conditionally use vite-dev.config.ts if --dev flag is present
if [ "$DEV_FLAG" == "--dev" ]; then
vite build --config vite-dev.config.ts
else
vite build
fi
echo "[BUILD:SH] Running post processor scripts..."
# Run Post processors: Update style imports in .js files, create component modules
node "$SCRIPT_DIR/post-processor.js" build/cjs $DEV_FLAG
node "$SCRIPT_DIR/post-processor.js" build/es $DEV_FLAG

View File

@@ -0,0 +1,54 @@
import { promises as fs } from "fs"
import { dirname, resolve } from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const modules = [
"Clusters.js",
"helper.js",
"IconController.js",
"IconEditor.js",
"IconInfo.js",
"Icons.js",
"IconsMerge.js",
"IconStyleSelector.js",
"IconTile.js",
"IconInfo.slice.js",
"Icons.slice.js",
]
const displayModuleNames = {
"IconInfo.slice": "IconInfoSlice",
"Icons.slice": "IconsSlice",
}
async function generateModule(fileName, isDev) {
if (modules.includes(fileName)) {
const dir = fileName.slice(0, -3)
const displayModuleName = displayModuleNames[dir] || dir
const name = `@armco/icon-spot/${displayModuleName}`
const packageJsonContent = {
name,
main: `../${isDev ? "build/" : ""}cjs/${dir}.js`,
module: `../${isDev ? "build/" : ""}es/${dir}.js`,
types: `../${isDev ? "build/" : ""}types/${dir}.d.ts`,
}
const dirPath = resolve(
__dirname,
`../${isDev ? "" : "build/"}${displayModuleName}`,
)
try {
await fs.mkdir(dirPath, { recursive: true })
await fs.writeFile(
resolve(dirPath, "package.json"),
JSON.stringify(packageJsonContent, null, 2),
)
} catch (error) {
console.error(`Error processing directory ${dirPath}:`, error)
}
}
}
export default generateModule

View File

@@ -0,0 +1,25 @@
import { readdir } from "fs/promises"
import generateModule from "./generate-module.js"
async function postProcessor(dir, isDev) {
try {
const files = await readdir(dir)
await Promise.all(
files.map(async (file) => {
await generateModule(file, isDev)
}),
)
} catch (error) {
console.error(`Error processing directory ${dir}:`, error)
}
}
const targetDir = process.argv[2]
const isDev = process.argv.includes("--dev")
if (targetDir) {
postProcessor(targetDir, isDev)
} else {
console.error("Please provide the build directory to run post processor on.")
process.exit(1)
}

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en" ar-theme="th-light-1">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React Redux App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" style="height:100vh;"></div>
<script type="module" src="/src/Test.tsx"></script>
</body>
</html>

7770
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

88
package.json Normal file
View File

@@ -0,0 +1,88 @@
{
"name": "@armco/icon-spot",
"version": "0.0.6",
"type": "module",
"main": "./build/cjs/index.js",
"module": "./build/es/index.js",
"types": "./build/types/index.d.ts",
"scripts": {
"dev": "NODE_ENV=development vite --config vite-run.config.ts",
"start": "serve -s build",
"build": "./build-tools/build.sh",
"build:sm": "./build-tools/build.sh --dev",
"publish:sh": "./publish.sh"
},
"peerDependencies": {
"@armco/components": "^0.0.60",
"@armco/configs": "^0.0.11",
"@armco/icon": "^0.0.10",
"@armco/types": "^0.0.18",
"@armco/utils": "^0.0.29",
"@reduxjs/toolkit": "^1.8.1",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1",
"react-dom": "^18.2.0",
"react-redux": "^8.0.1",
"react-router-dom": "^6.13.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": "error",
"react/jsx-no-target-blank": "off"
}
},
"prettier": "prettier-config-nick",
"repository": {
"type": "git",
"url": "git+https://github.com/ReStruct-Corporate-Advantage/icon-spot.git"
},
"keywords": [
"components",
"atomic",
"building-blocks",
"foundation"
],
"license": "ISC",
"bugs": {
"url": "https://github.com/ReStruct-Corporate-Advantage/icon-spot/issues"
},
"homepage": "https://github.com/ReStruct-Corporate-Advantage/icon-spot#readme",
"devDependencies": {
"@armco/components": "^0.0.60",
"@armco/configs": "^0.0.15",
"@armco/icon": "^0.0.13",
"@armco/shared-components": "^0.0.61",
"@armco/types": "^0.0.22",
"@armco/utils": "^0.0.31",
"@reduxjs/toolkit": "^2.11.2",
"@types/node": "^24.0.0",
"@types/react": "^18.3.26",
"@types/react-dom": "^19.2.3",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^5.1.0",
"ajv": "^8.17.1",
"dotenv": "^17.2.3",
"glob": "^11.0.3",
"immer": "^11.1.3",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-redux": "^9.2.0",
"react-router-dom": "^7.11.0",
"sass-embedded": "^1.97.1",
"typescript": "^5.9.3",
"uuid": "^13.0.0",
"vite": "^7.3.0",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-externalize-deps": "^0.10.0",
"vite-plugin-lib-inject-css": "^2.2.2",
"vitest": "^4.0.8"
}
}

34
publish.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/sh
semver=${1:-patch}
set -e
npm --no-git-tag-version version ${semver}
npm run build
cp package.json build/
# Use Node.js for portable package.json normalization
PKG_PATH="$(pwd)/build/package.json" node - <<'EOF'
const fs = require('fs');
const path = process.env.PKG_PATH;
const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
pkg.private = false;
delete pkg.scripts;
delete pkg.devDependencies;
if (!pkg.files) pkg.files = ['*'];
else pkg.files = pkg.files.map(x => x === 'build' ? '*' : x);
['main','module','types'].forEach(k => {
if (pkg[k]) {
pkg[k] = pkg[k]
.replace(/^\.?\/build\//, '')
.replace(/^build\//, '');
}
});
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
EOF
cd build
npm publish --access public --loglevel verbose

34
src/Clusters.tsx Executable file
View File

@@ -0,0 +1,34 @@
import { useEffect, useState } from "react"
import { getStatic } from "@armco/utils/network"
const Clusters = (): JSX.Element => {
const [clusters, setClusters] = useState()
useEffect(() => {
const getClusters = async () => {
const response = await getStatic("/icon/clusters")
setClusters(response.body)
}
getClusters()
}, [])
return (
<div className="ar-Clusters">
{clusters &&
(clusters as Array<Array<string>>).map(
(cluster: Array<string>, index: number) => {
return (
<div className="ar-Clusters__cluster">
<div className="ar-Clusters__cluster-header">
{"Cluster" + index}
</div>
{cluster.map((url) => (
<img src={url} alt="cluster-icon" width="32" />
))}
</div>
)
},
)}
</div>
)
}
export default Clusters

View File

@@ -0,0 +1,28 @@
.ar-FavoritesList {
max-width: 0;
transition: max-width 0.3s;
&.show {
max-width: 10rem;
}
.ar-FavoritesList__header {
background-color: var(--ar-color-prominent);
&.loggedIn {
background-color: var(--ar-color-success);
}
}
&.shrink {
max-width: 4rem;
}
.ar-FavoriteItem {
width: 0;
transition: width 0.3s linear;
&.show {
width: 5rem;
}
}
}

124
src/FavoritesList.tsx Executable file
View File

@@ -0,0 +1,124 @@
import { useEffect, useState } from "react"
import { useSelector } from "react-redux"
import {
ArButtonVariants,
ArPopoverPositions,
ArPopoverTriggers,
ArSizes,
FavoritesItemProps,
FavoritesListProps,
IconTileProps,
} from "./types"
import { useLoggedIn, usePanelContent } from "@armco/utils/hooks"
import { Button, Popover } from "@armco/shared-components"
import Icon from "@armco/icon"
import { getFavorites } from "./Icons.slice"
import IconTile from "./IconTile"
import "./FavoritesList.component.scss"
const FavoriteItem = (props: FavoritesItemProps): JSX.Element | null => {
const { index, favorite } = props
const [shown, show] = useState<boolean>()
useEffect(() => {
show(true)
return () => {
show(false)
setTimeout(() => { }, 300)
}
}, [])
return (
<span
className={`ar-FavoriteItem d-flex flex-column${shown ? " show" : ""}`}
>
<IconTile key={index} icon={favorite.icon} hideFooter hideBorder />
</span>
)
}
const FavoritesList = (props: FavoritesListProps): JSX.Element => {
const favorites = useSelector(getFavorites)
const { isLoggedIn } = useLoggedIn()
const { setPanelContent: setRightPanelContent } = usePanelContent(false)
const icons =
favorites &&
favorites.map((favorite: IconTileProps, index: number) => (
<FavoriteItem index={index} favorite={favorite} />
))
return (
<div
className={`ar-FavoritesList h-100${icons?.length > 0 ? " show" : ""}${isLoggedIn ? " shrink" : ""
}`}
>
<Icon icon="tb/TbLayoutSidebarRightCollapse" />
{!isLoggedIn ? (
<Popover
trigger={ArPopoverTriggers.HOVER}
position={ArPopoverPositions.LEFTBOTTOM}
version="v1"
>
<span
className="ar-FavoritesList__header p-2 border-bottom d-flex justify-content-center w-100"
slot="anchor"
>
<Icon
icon="io/IoIosWarning"
attributes={{ colors: { fillColor: "white" } }}
/>
</span>
<div slot="popover" className="p-2">
<strong className="mb-2 d-inline-block">
You're not logged in, all of your favorites will be lost when the
session is over.
</strong>
<p className="mb-0">
Please login to ensure your changes are saved!
</p>
</div>
</Popover>
) : (
<Popover
trigger={ArPopoverTriggers.HOVER}
position={ArPopoverPositions.LEFT}
>
<span
className="ar-FavoritesList__header loggedIn p-2 border-bottom d-flex justify-content-center w-100"
slot="anchor"
>
<Icon
icon="io/IoIosCheckmarkCircleOutline"
attributes={{ colors: { fillColor: "white" } }}
/>
</span>
<div slot="popover" className="p-2">
<strong className="mb-2 d-inline-block">
Your favorites are being synchronized with your account!
</strong>
</div>
</Popover>
)}
<div
className={`ar-FavoritesList__favorites flex-v-center flex-column mt-3${icons?.length > 0 ? " px-2" : ""
}`}
>
{icons}
</div>
{!isLoggedIn && (
<Button
classes="mx-2"
variant={ArButtonVariants.WARNING}
size={ArSizes.SMALL}
content="Login"
preIcon="ci/CiWarning"
onClick={() => {
setRightPanelContent({ componentName: "LoginProvider" })
}}
/>
)}
</div>
)
}
export default FavoritesList

View File

@@ -0,0 +1,48 @@
.ar-IconController {
.ar-IconController__main, .ar-IconController__header {
background-color: var(--ar-bg-base);
}
.ar-IconController__color-palette {
border-radius: 50%;
}
.ar-IconController__download-button, .ar-IconController__animate-button {
background-color: orange;
border-color: orange;
}
.ar-IconController__main {
border-radius: 0.3rem;
}
.ar-IconController__controls-form {
background-color: var(--ar-bg);
}
.hover-show {
cursor: pointer;
.ar-IconController__download-icon {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
&:hover {
.ar-IconController__download-icon {
opacity: 1;
pointer-events: auto;
transition: box-shadow 0.3s;
&:hover {
box-shadow: 0 4px 8px var(--ar-shadow);
border-radius: 3px
}
}
}
}
.ar-IconController__meta-item {
color: var(--ar-color-secondary);
}
}

375
src/IconController.tsx Executable file
View File

@@ -0,0 +1,375 @@
import { useNavigate } from "react-router-dom"
import { useEffect, useState } from "react"
import { useSelector } from "react-redux"
import {
ArButtonVariants,
ArPopoverSlots,
IconControllerProps,
ObjectType,
} from "./types"
import { API_CONFIG } from "@armco/configs/endpoints"
import { getStatic, post } from "@armco/utils/network"
import { usePanelContent } from "@armco/utils/hooks"
import {
Breadcrumb,
Button,
Select,
Suggestions,
Tags,
TextInput,
} from "@armco/shared-components"
import Icon from "@armco/icon"
import { getIconStyles } from "./IconInfo.slice"
import IconEditor from "./IconEditor"
import { complement, downloadSvg } from "./helper"
import "./IconController.component.scss"
const STATIC_ROOT = API_CONFIG.STATIC_HOST[process.env.NODE_ENV]
const IconController = (props: IconControllerProps): JSX.Element => {
const { group, icon, name } = props
const [size, setSize] = useState<string>()
const [unit, setUnit] = useState<string>("rem")
const [similarIcons, setSimilarIcons] = useState<Array<string>>()
const iconStyles = useSelector(getIconStyles)
const { setPanelContent: setRightPanelContent } = usePanelContent(false)
const fontColor = complement(iconStyles?.bgColor || "white")
const navigate = useNavigate()
// const suggestions = getSuggestions(group, name)
useEffect(() => {
const filename = `${group}_${name}-black.png`
getStatic(`/static/${filename}`).then((res) => {
const imgBlob = res.body
const formData = new FormData()
const file = new File([imgBlob], filename, { type: imgBlob.type })
formData.append("file", file)
formData.append("count", "20")
post(
"http://localhost:5002/api/similar-icon-paths",
formData,
undefined,
{ headers: { "Content-Type": "multipart/form-data" } },
true,
true,
).then((res) => {
if (res.status === 200) {
const iconPaths = res.body.map((str: string) =>
str.replace(
"../images",
`${API_CONFIG.STATIC_HOST[process.env.NODE_ENV]}/static`,
),
)
// setSimilarIcons(iconPaths)
post(STATIC_ROOT + "/icon/png-to-svg", {
urls: iconPaths,
}).then((res) => {
setSimilarIcons(res.body)
})
}
})
})
}, [name, group])
return (
<div className="ar-IconController container h-100 overflow-auto">
{group && name && (
<Breadcrumb
data={[
{ label: group, route: `/icon/${group}` },
{ label: name, route: `/icon/${group}/${name}` },
]}
/>
)}
<div className="ar-IconController__header mb-3 p-3 border flex-v-center">
<h6 className="mb-0 fw-bold">
{group}.{name}
</h6>
<Button
classes="ar-IconController__animate-button me-3 ms-auto"
variant={ArButtonVariants.PRIMARY}
postIcon="md/MdAnimation"
content="Animate"
onClick={() =>
navigate("/icon/animate", {
state: { icon, iconStyles, name: `${group}_${name}` },
})
}
/>
<Button
classes="ar-IconController__download-button me-3"
variant={ArButtonVariants.PRIMARY}
postIcon="md/MdDownload"
content="Download"
onClick={() =>
downloadSvg(
(icon as ObjectType).icon as string,
name || "download.svg",
{
size: "3rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
},
)
}
/>
<Icon
icon="io/IoIosColorPalette"
slot={ArPopoverSlots.ANCHOR}
attributes={{
classes:
"ar-IconController__color-palette cursor-pointer hover-shadow",
size: "2rem",
colors: { fillColor: "orange" },
}}
events={{
onClick: () => setRightPanelContent({ component: <IconEditor /> }),
}}
/>
</div>
<div className="ar-IconController__main mb-3 p-3 border">
<div className="row">
<div
className="ar-IconController__icon-sizes col"
style={{ backgroundColor: iconStyles?.bgColor }}
>
{group && name && (
<div className="row h-100 border-right">
<div className="col flex-center flex-column border-right">
<div className="h-50 flex-center flex-column border-bottom w-100 position-relative hover-show">
<Icon
icon="md/MdDownload"
attributes={{
classes:
"ar-IconController__download-icon position-absolute top-1 end-1",
colors: { fillColor: "orange" },
size: "2.5rem",
}}
events={{
onClick: () =>
downloadSvg(
(icon as ObjectType).icon as string,
name,
{
size: "3rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
},
),
}}
/>
<Icon
key={`${group}/${name}`}
icon={`${group}/${name}`}
attributes={{
size: "3rem",
colors: {
fillColor: iconStyles?.fillColor || "royalblue",
strokeColor: iconStyles?.strokeColor,
},
strokeWidth: iconStyles?.strokeWidth,
}}
fillPath
/>
<span
className="fw-bold"
style={{ color: fontColor || "black" }}
>
3rem
</span>
</div>
<div className="h-50 flex-center flex-column w-100 position-relative hover-show">
<Icon
icon="md/MdDownload"
attributes={{
classes:
"ar-IconController__download-icon position-absolute top-1 end-1",
colors: { fillColor: "orange" },
size: "2.5rem",
}}
events={{
onClick: () =>
downloadSvg(
(icon as ObjectType).icon as string,
name,
{
size: "7rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
},
),
}}
/>
<Icon
key={`${group}/${name}`}
icon={`${group}/${name}`}
attributes={{
size: "7rem",
colors: {
fillColor: iconStyles?.fillColor || "royalblue",
strokeColor: iconStyles?.strokeColor,
},
strokeWidth: iconStyles?.strokeWidth,
}}
fillPath
/>
<span
className="fw-bold"
style={{ color: fontColor || "black" }}
>
7rem
</span>
</div>
</div>
<div className="col flex-center flex-column position-relative hover-show">
<Icon
icon="md/MdDownload"
attributes={{
classes:
"ar-IconController__download-icon position-absolute top-1 end-1",
colors: { fillColor: "orange" },
size: "2.5rem",
}}
events={{
onClick: () =>
downloadSvg((icon as ObjectType).icon as string, name, {
size: size && unit ? size + unit : "20rem",
...iconStyles,
color: iconStyles?.strokeColor || "royalblue",
}),
}}
/>
<Icon
key={`${group}/${name}`}
icon={`${group}/${name}`}
attributes={{
size: `${size}${unit}` || "20rem",
colors: {
fillColor: iconStyles?.fillColor || "royalblue",
strokeColor: iconStyles?.strokeColor,
},
strokeWidth: iconStyles?.strokeWidth,
}}
fillPath
/>
<span
className="fw-bold"
style={{ color: fontColor || "black" }}
>
{size && unit ? size + unit : "20rem"}
</span>
</div>
</div>
)}
</div>
<div className="ar-IconController__controls col d-flex">
<div className="row w-100">
{icon && (
<div className="ar-IconController__meta col-4">
<h6>Icon Details</h6>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Created By: </strong>
{icon.createdby as string}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Description: </strong>
{icon.description as string}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Downloaded: </strong>
{((icon.meta as ObjectType)?.downloadedTimes as number) ||
0}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Liked: </strong>
{((icon.meta as ObjectType)?.downloadedTimes as number) ||
0}
</div>
</div>
<div className="ar-IconController__meta-item row lh-1-5">
<div className="col">
<strong>Size: </strong>
{(icon.meta as ObjectType)?.size as number}
</div>
</div>
</div>
)}
<div
className={`ar-IconController__controls-form ${icon ? "col-8" : "col-12"
} border`}
>
<div className="row h-100 py-3">
<div className="ar-IconController__controls-form-container col-12 h-25">
<div className="row">
<div className="col">
<TextInput
type="slider"
label="Icon Size"
min={1}
max={30}
onChange={(e) =>
setSize((e.target as HTMLInputElement).value)
}
/>
</div>
<div className="col">
<Select
label="Unit"
onSelectionChanged={(e: { value: string }) =>
setUnit(e.value)
}
options={[
{ label: "rem", value: "rem" },
{ label: "px", value: "px" },
{ label: "vh", value: "vh" },
]}
/>
</div>
</div>
</div>
{/* <Tags classes="col-12 h-100 px-0" label={name} /> */}
{icon && icon.tags && (
<Tags
// clickHandler={clickHandler}
classes="col-12 h-75 px-0"
label={name}
tags={Object.fromEntries(
(icon.tags as Array<string>).map((tag) => [tag, 1]),
)}
/>
)}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Similar Suggestions */}
<Suggestions
classes="border mb-3"
suggestions={similarIcons}
title={`Icons Similar to ${name}`}
inSvgString
/>
{/* Recently Visited */}
{/* <Suggestions
classes="border mb-3"
suggestions={similarIcons}
title="Trending Icons"
inSvgString
/> */}
</div>
)
}
export default IconController

47
src/IconEditor.tsx Executable file
View File

@@ -0,0 +1,47 @@
import { useEffect, useState } from "react"
import { useDispatch } from "react-redux"
import { v4 as uuid } from "uuid"
import { IconEditorProps } from "./types"
import { setIconStyles } from "./IconInfo.slice"
import IconStyleSelector from "./IconStyleSelector"
const iconEditorTabs = [
{
label: "Icon",
id: uuid(),
},
{
label: "Background",
id: uuid(),
},
]
const IconEditor = (props: IconEditorProps): JSX.Element => {
const { layout } = props
const dispatch = useDispatch()
const [iconStyles, setIconStylesState] = useState<{
fillColor?: string
strokeColor?: string
bgColor?: string
strokeWidth?: string
}>()
useEffect(() => {
iconStyles && dispatch(setIconStyles(iconStyles))
}, [iconStyles, dispatch])
return (
<div className="ar-IconEditor h-100">
<div className="ar-IconEditor__config-form h-100 overflow-auto">
<IconStyleSelector
iconStyles={iconStyles}
setIconStyles={setIconStylesState}
layout={layout}
/>
</div>
</div>
)
}
export default IconEditor

24
src/IconInfo.slice.ts Normal file
View File

@@ -0,0 +1,24 @@
import { PayloadAction, createSlice } from "@reduxjs/toolkit"
import { IconStyles } from "./types"
export interface IconPageState {
iconStyles?: IconStyles
}
const initialState: IconPageState = {}
export const iconInfoSlice = createSlice({
name: "iconPage",
initialState,
reducers: {
setIconStyles: (state, action: PayloadAction<IconStyles | undefined>) => {
state.iconStyles = action.payload
},
},
})
export const { setIconStyles } = iconInfoSlice.actions
export const getIconStyles = (state: any) => state.iconInfo.iconStyles
export default iconInfoSlice.reducer

24
src/IconInfo.tsx Executable file
View File

@@ -0,0 +1,24 @@
import { useLocation, useParams } from "react-router-dom"
import { Main } from "@armco/shared-components"
import { usePanelContent } from "@armco/utils/hooks"
import IconController from "./IconController"
const IconInfo = (): JSX.Element => {
const { panelContent: rightPanelContent } = usePanelContent(false)
const { group, name } = useParams()
const location = useLocation()
return (
<div className="ar-IconInfo h-100">
<Main
classes="h-100"
mainContent={
<IconController group={group} name={name} icon={location.state} />
}
rightPanelContent={rightPanelContent}
/>
</div>
)
}
export default IconInfo

View File

@@ -0,0 +1,12 @@
.ar-IconMergeContainer {
svg {
width: 5rem;
height: 5rem;
margin-left: 2rem;
margin-right: 2rem;
path:hover {
outline: 1px dotted grey;
}
}
}

157
src/IconMergeContainer.tsx Executable file
View File

@@ -0,0 +1,157 @@
import { useEffect, useRef } from "react"
import { ArrayType, IconMergeContainerProps } from "./types"
import "./IconMergeContainer.component.scss"
const iconTest = [
{
name: "CiAlignBottom",
icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><g id="Align_Bottom"><g><path d="M3.548,20.922h16.9a.5.5,0,0,0,0-1H3.548a.5.5,0,0,0,0,1Z"></path><path d="M9,18.919H6.565a2.5,2.5,0,0,1-2.5-2.5V5.578a2.5,2.5,0,0,1,2.5-2.5H9a2.5,2.5,0,0,1,2.5,2.5V16.419A2.5,2.5,0,0,1,9,18.919ZM6.565,4.078a1.5,1.5,0,0,0-1.5,1.5V16.419a1.5,1.5,0,0,0,1.5,1.5H9a1.5,1.5,0,0,0,1.5-1.5V5.578A1.5,1.5,0,0,0,9,4.078Z"></path><path d="M17.437,18.919H15a2.5,2.5,0,0,1-2.5-2.5V10.55A2.5,2.5,0,0,1,15,8.05h2.434a2.5,2.5,0,0,1,2.5,2.5v5.869A2.5,2.5,0,0,1,17.437,18.919ZM15,9.05a1.5,1.5,0,0,0-1.5,1.5v5.869a1.5,1.5,0,0,0,1.5,1.5h2.434a1.5,1.5,0,0,0,1.5-1.5V10.55a1.5,1.5,0,0,0-1.5-1.5Z"></path></g></g></svg>',
createdby: "Armco",
createdAt: "2023-10-14T20:46:51.541Z",
updatedAt: "2023-10-14T20:46:51.541Z",
meta: {
size: "772 bytes",
downloadTimes: 0,
favoriteTimes: 0,
},
group: "ci",
description: "",
tags: [
{
name: "Ci",
state: "verified",
},
{
name: "Align",
state: "verified",
},
{
name: "Bottom",
state: "verified",
},
{
name: "icon",
state: "verified",
},
{
name: "armco",
state: "verified",
},
],
},
{
name: "CiAlignCenterH",
icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><g id="Align_Center-H"><path d="M17.42,4.062H12.5v-.51a.5.5,0,0,0-1,0v.51H6.58a2.507,2.507,0,0,0-2.5,2.5V9a2.5,2.5,0,0,0,2.5,2.5H11.5v1H9.06A2.507,2.507,0,0,0,6.56,15v2.44a2.507,2.507,0,0,0,2.5,2.5H11.5v.51a.5.5,0,0,0,1,0v-.51h2.43a2.5,2.5,0,0,0,2.5-2.5V15a2.5,2.5,0,0,0-2.5-2.5H12.5v-1h4.92A2.5,2.5,0,0,0,19.92,9V6.562A2.507,2.507,0,0,0,17.42,4.062ZM11.5,18.942H9.06a1.511,1.511,0,0,1-1.5-1.5V15a1.5,1.5,0,0,1,1.5-1.5H11.5Zm0-8.44H6.58A1.5,1.5,0,0,1,5.08,9V6.562a1.5,1.5,0,0,1,1.5-1.5H11.5Zm3.43,3a1.5,1.5,0,0,1,1.5,1.5v2.44a1.5,1.5,0,0,1-1.5,1.5H12.5V13.5ZM18.92,9a1.5,1.5,0,0,1-1.5,1.5H12.5V5.062h4.92a1.5,1.5,0,0,1,1.5,1.5Z"></path></g></svg>',
createdby: "Armco",
createdAt: "2023-10-14T20:46:51.541Z",
updatedAt: "2023-10-14T20:46:51.541Z",
meta: {
size: "790 bytes",
downloadTimes: 0,
favoriteTimes: 0,
},
group: "ci",
description: "",
tags: [
{
name: "Ci",
state: "verified",
},
{
name: "Align",
state: "verified",
},
{
name: "Center",
state: "verified",
},
{
name: "H",
state: "verified",
},
{
name: "icon",
state: "verified",
},
{
name: "armco",
state: "verified",
},
],
},
{
name: "CiAlignCenterV",
icon: '<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><g id="Align_Center-V"><path d="M20.446,11.5h-.51V9.07a2.5,2.5,0,0,0-2.5-2.5h-2.43a2.5,2.5,0,0,0-2.5,2.5V11.5H11.5V6.58A2.5,2.5,0,0,0,9,4.08H6.566a2.5,2.5,0,0,0-2.5,2.5V11.5h-.52a.5.5,0,0,0,0,1h.52v4.92a2.5,2.5,0,0,0,2.5,2.5H9a2.5,2.5,0,0,0,2.5-2.5V12.5h1.01v2.43a2.5,2.5,0,0,0,2.5,2.5h2.43a2.5,2.5,0,0,0,2.5-2.5V12.5h.51A.5.5,0,0,0,20.446,11.5ZM10.5,17.42A1.5,1.5,0,0,1,9,18.92H6.566a1.5,1.5,0,0,1-1.5-1.5V12.5H10.5Zm0-5.92H5.066V6.58a1.5,1.5,0,0,1,1.5-1.5H9a1.5,1.5,0,0,1,1.5,1.5Zm8.44,3.43a1.5,1.5,0,0,1-1.5,1.5h-2.43a1.5,1.5,0,0,1-1.5-1.5V12.5h5.43Zm0-3.43h-5.43V9.07a1.5,1.5,0,0,1,1.5-1.5h2.43a1.5,1.5,0,0,1,1.5,1.5Z"></path></g></svg>',
createdby: "Armco",
createdAt: "2023-10-14T20:46:51.541Z",
updatedAt: "2023-10-14T20:46:51.541Z",
meta: {
size: "784 bytes",
downloadTimes: 0,
favoriteTimes: 0,
},
group: "ci",
description: "",
tags: [
{
name: "Ci",
state: "verified",
},
{
name: "Align",
state: "verified",
},
{
name: "Center",
state: "verified",
},
{
name: "V",
state: "verified",
},
{
name: "icon",
state: "verified",
},
{
name: "armco",
state: "verified",
},
],
},
]
const parser = new DOMParser()
const IconMergeContainer = (props: IconMergeContainerProps): JSX.Element => {
// const { icons } = props
const svgRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
const childList: Array<SVGSVGElement> = []
iconTest?.forEach((icon) => {
const svgElement = parser
.parseFromString(icon.icon, "image/svg+xml")
.querySelector("svg")
if (svgElement) {
childList.push(svgElement)
}
})
svgRef.current?.replaceChildren(...childList)
const paths = childList.reduce((acc: ArrayType, svgElement) => {
const pathList = svgElement.querySelectorAll("path")
console.log(pathList)
pathList.forEach((pathItem) => {
pathItem.setAttribute("draggable", "true")
pathItem.addEventListener("dragstart", (e) => console.log(e))
})
return acc.concat(Array.from(pathList) as any)
}, [])
; (paths[0] as any).parentElement?.replaceChildren(...paths)
}, [svgRef])
return <div className="ar-IconMergeContainer" ref={svgRef} />
}
export default IconMergeContainer

83
src/IconStyleSelector.tsx Executable file
View File

@@ -0,0 +1,83 @@
import { v4 as uuid } from "uuid"
import { IconStyles, IconStyleSelectorProps } from "./types"
import { AdvancedColorPicker, Slider } from "@armco/shared-components"
const fillColorId = uuid()
const strokeColorId = uuid()
const bgColorId = uuid()
const IconStyleSelector = (props: IconStyleSelectorProps): JSX.Element => {
const { layout, setIconStyles } = props
const setIconStylesLocal = (propName: string, propValue: string) => {
setIconStyles((currentIconStyles: IconStyles | undefined) => ({
...currentIconStyles,
[propName]: propValue,
}))
}
const fill = (
<AdvancedColorPicker
key="fill-color-selector"
id={fillColorId}
displaySample
onColorSelect={(color) => setIconStylesLocal("fillColor", color)}
title="Fill Color"
/>
)
const strokeWidth = (
<Slider
containerClasses="py-2 border-top border-bottom mb-4"
label="Stroke Width"
min={1}
max={10}
onChange={(e) => setIconStylesLocal("strokeWidth", e.target.value)}
withManual
/>
)
const stroke = (
<AdvancedColorPicker
key="stoke-color-selector"
id={strokeColorId}
displaySample
onColorSelect={(color) => setIconStylesLocal("strokeColor", color)}
title="Stroke Color"
/>
)
const bg = (
<AdvancedColorPicker
key="bg-color-selector"
id={bgColorId}
displaySample
onColorSelect={(color) => setIconStylesLocal("bgColor", color)}
title="Background Color"
/>
)
return (
<div className="ar-IconStyleSelector p-3 w-100 overflow-auto">
{layout === "horizontal" ? (
<>
<div className="d-flex">
<div className="px-3">{fill}</div>
<div className="px-3">{stroke}</div>
</div>
<div className="row">
<div className="col">{strokeWidth}</div>
</div>
</>
) : (
<>
{fill}
{strokeWidth}
{stroke}
{bg}
</>
)}
</div>
)
}
export default IconStyleSelector

86
src/IconTile.component.scss Executable file
View File

@@ -0,0 +1,86 @@
.ar-IconTile {
overflow: hidden;
.font-blue {
color: blue;
}
&.selectable .ar-IconTile__image {
border: 3px solid var(--ar-color-border);
border-radius: 3px;
}
.font-black {
color: black;
}
.ar-IconTile__select-icon {
top: 0.3rem;
right: 0.3rem;
}
.ar-ToolBar {
z-index: 1;
top: 0;
right: -1.5rem;
width: 1.5rem;
height: 100%;
transition: right 0.3s;
flex-direction: column;
background-color: var(--ar-bg-invert-fade);
border-left: 1px solid lightblue;
&.show {
right: 0
}
&.top {
top: -1.5rem;
height: 1.5rem;
width: 100%;
transition: top 0.3s;
&.show {
top: 0
}
}
&.left {
left: -1.5rem;
width: 1.5rem;
height: 100%;
transition: left 0.3s;
&.show {
left: 0
}
}
&.bottom {
bottom: -1.5rem;
height: 1.5rem;
width: 100%;
transition: bottom 0.3s;
&.show {
bottom: 0
}
}
.ar-ToolItem {
flex: 1;
border-top: 1px solid white;
border-bottom: 1px solid white;
}
}
&.list {
}
&.compact {
}
.ar-IconTile__footer {
color: grey;
font-size: 0.6rem;
text-align: center;
}
}

158
src/IconTile.tsx Executable file
View File

@@ -0,0 +1,158 @@
import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import {
ArThemes,
ArIconTileTypes,
ArIconSourceTypes,
IconTileProps,
} from "./types"
import { ICON_ROOT } from "@armco/configs/endpoints"
import { useTheme } from "@armco/utils/hooks"
import Icon from "@armco/icon"
import "./IconTile.component.scss"
const IconTile = (props: IconTileProps): JSX.Element => {
const {
classes,
// fillColor,
hideBorder,
hideFooter,
icon,
iconSize,
onClick,
selectable,
strokeColor,
strokeWidth,
// tools,
// toolsPlacement,
type,
} = props
const [hovered, setHovered] = useState<boolean>()
const [isSelected, toggleSelected] = useState<boolean>()
const { theme } = useTheme()
const navigate = useNavigate()
useEffect(() => {
if (!selectable && isSelected) {
toggleSelected(false)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectable])
// const generateTools = (tools: Array<ToolItemConfig>) => {
// return tools.map((tool: ToolItemConfig, index: number) => {
// const anchor = (
// <Icon
// slot={ArPopoverSlots.ANCHOR}
// {...{
// ...tool.iconProps,
// onClick: tool.onClick,
// }}
// />
// )
// return (
// <span
// key={"tool-item-" + icon.name + "-" + index}
// className="ar-ToolItem flex-center"
// onClick={(e) => {
// e.stopPropagation()
// }}
// >
// {tool.children ? (
// <Popover
// // trigger={ArPopoverTriggers.HOVER}
// position={ArPopoverPositions.RIGHTBOTTOM}
// version="v1"
// >
// {anchor}
// <div slot={ArPopoverSlots.POPOVER}>
// {generateTools(tool.children)}
// </div>
// </Popover>
// ) : (
// anchor
// )}
// </span>
// )
// })
// }
// const iconTools = tools && (
// <div
// className={`ar-ToolBar position-absolute${
// toolsPlacement ? " " + toolsPlacement : ""
// }${selectable ? " d-none" : " d-flex"}${hovered ? " show" : ""}${
// type ? " " + type : ""
// }`}
// >
// {generateTools(tools)}
// </div>
// )
return (
<span
className={`ar-IconTile d-inline-block mx-2${hideBorder ? "" : " border"
}${classes ? " " + classes : ""}${type === ArIconTileTypes.LIST ? " d-flex" : ""
}${selectable ? " selectable" : ""}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={() => onClick && onClick(icon)}
>
<div
className={`ar-IconTile__image position-relative h-100${type === ArIconTileTypes.LIST ? " flex-v-center px-1" : " flex-center"
}`}
onClick={() =>
selectable
? toggleSelected(!isSelected)
: navigate(`/icon/${icon.group}/${icon.name}`, { state: icon })
}
>
{selectable && (
<Icon
icon={
isSelected ? "im/ImCheckboxChecked" : "im/ImCheckboxUnchecked"
}
attributes={{
classes: "ar-IconTile__select-icon position-absolute",
}}
/>
)}
<Icon
// key={icon.name}
icon={{ source: btoa(icon.icon), type: ArIconSourceTypes.b64 }}
attributes={{
size: iconSize,
colors: {
fillColor: hovered
? "royalblue"
: theme === ArThemes.LIGHT1
? "grey"
: "white",
strokeColor,
},
strokeWidth,
}}
fillPath
hoverShadow
/>
{/* {type !== ArIconTileTypes.LIST && iconTools} */}
</div>
{!hideFooter && (
<footer
className={`ar-IconTile__footer fw-bold text-nowrap overflow-hidden flex-center px-2 ${hovered ? "font-blue btn-link" : "font-black"
}${type === ArIconTileTypes.LIST ? " flex-grow-1" : ""}`}
onClick={() => {
const iconLink = `${ICON_ROOT}/${icon.group}/${icon.name}`
selectable
? toggleSelected(!isSelected)
: navigator.clipboard.writeText(iconLink)
}}
>
{icon.name}
</footer>
)}
</span>
)
}
export default IconTile

34
src/Icons.slice.ts Normal file
View File

@@ -0,0 +1,34 @@
import { PayloadAction, createSlice, Slice } from "@reduxjs/toolkit"
import { IconTileProps } from "./types"
export interface IconsPageState {
favorites: Array<IconTileProps>
selectedTag?: string
}
const initialState: IconsPageState = {
favorites: [],
}
export const iconsSlice: Slice<IconsPageState> = createSlice({
name: "iconsPage",
initialState,
reducers: {
setFavorites: (state, action: PayloadAction<IconTileProps>) => {
state.favorites = [...state.favorites, action.payload]
},
removeFavorite: (state, action: PayloadAction<IconTileProps>) => {
state.favorites.splice(state.favorites.indexOf(action.payload))
},
selectTag: (state, action: PayloadAction<string>) => {
state.selectedTag = action.payload
},
},
})
export const { selectTag, setFavorites, removeFavorite } = iconsSlice.actions
export const getFavorites = (state: any) => state.icons.favorites
export const getSelectedTag = (state: any) => state.icons.selectedTag
export default iconsSlice.reducer

59
src/Icons.tsx Executable file
View File

@@ -0,0 +1,59 @@
import { useEffect, useState } from "react"
import { useDispatch } from "react-redux"
import { FilterState, IconResponse, ObjectType } from "./types"
import { usePanelContent } from "@armco/utils/hooks"
import { getStatic } from "@armco/utils/network"
import { Filters, Main } from "@armco/shared-components"
import { selectTag } from "./Icons.slice"
import IconsList from "./IconsList"
const fetchData = async (api: string, cb: Function) => {
const response: { status: number; body: Array<IconResponse> } =
await getStatic(api, null, null)
cb && cb(response.body)
}
const Icons = (): JSX.Element => {
const [tags, setTags] = useState<ObjectType>()
const [filters, setFilters] = useState<FilterState | undefined>({
count: {
value: "1to1000",
data: { startIndex: 0, endIndex: 999 },
},
})
const { panelContent: rightPanelContent } = usePanelContent(false)
const dispatch = useDispatch()
useEffect(() => {
const variant = "gt100"
const parseTags = (tagReponse: ObjectType) => {
setTags(tagReponse[variant] as ObjectType)
}
!tags && fetchData(`/icon/tag/all?variants=${variant}`, parseTags)
}, [tags])
const clickHandler = (e: any) => {
dispatch(selectTag(e.point.name))
}
return (
<div className="ar-Icons d-flex flex-column h-100">
<Main
contentClasses="p-2"
drawerContent={
<Filters
config={{ tags }}
clickHandler={clickHandler}
initialFilters={filters}
onFilterChange={setFilters}
/>
}
mainContent={<IconsList />}
rightPanelContent={rightPanelContent}
rightPanelHeader="Favorites"
/>
</div>
)
}
export default Icons

67
src/IconsList.component.scss Executable file
View File

@@ -0,0 +1,67 @@
.ar-IconsList {
.ar-IconsList__header-pagination-search-upload {
background-color: var(--ar-bg);
color: var(--ar-colora);
}
.ar-IconList__selection-manager {
background-color: var(--ar-bg);
color: var(--ar-colora);
transition: all 0.3s;
&.hide {
height: 0;
overflow: hidden;
padding: 0 !important;
border: none !important;
}
}
.ar-IconsList__icon-tile-container {
background-color: var(--ar-bg);
color: var(--ar-color);
}
.ar-IconTile {
cursor: pointer;
width: 7rem;
height: 7rem;
transition: width 0.3s, padding 0.3s;
&.compact {
width: 4rem;
height: 4rem;
}
&.list {
width: 8rem;
height: 2rem;
}
&:hover {
background-color: var(--ar-bg-hover);
}
footer {
height: 1.9rem;
border-top: 1px solid var(--bs-border-color);
background-color: var(--ar-bg-base);
}
}
.ar-IconsList__upload-button {
background-color: orange;
border-color: orange;
}
.ar-IconsList__color-palette {
border-radius: 50%;
}
@media screen and (max-width: 576px) {
.ar-IconTile {
width: 3rem;
height: 3rem;
}
}
}

442
src/IconsList.tsx Executable file
View File

@@ -0,0 +1,442 @@
import { ChangeEvent, useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { useDispatch, useSelector } from "react-redux"
import { v4 as uuid } from "uuid"
import {
ArButtonVariants,
ArIconTileTypes,
ArLoaderTypes,
ArPopoverSlots,
ArPopoverTriggers,
ArSizes,
FunctionType,
IconResponse,
IconStyles,
IconTileProps,
IconsListProps,
ObjectType,
SearchItem,
SegmentData,
} from "./types"
import { API_CONFIG, ENDPOINTS } from "@armco/configs/endpoints"
import { useNotification, usePanelContent } from "@armco/utils/hooks"
import { get } from "@armco/utils/network"
import { debounce } from "@armco/utils/helper"
import Icon from "@armco/icon"
import {
Button,
Loader,
MenuButton,
Pagination,
Pillbox,
Popover,
SearchField,
SegmentedControl,
} from "@armco/shared-components"
import {
getFavorites,
getSelectedTag,
removeFavorite,
setFavorites,
} from "./Icons.slice"
import { getIconStyles, setIconStyles } from "./IconInfo.slice"
import IconStyleSelector from "./IconStyleSelector"
import IconTile from "./IconTile"
import "./IconsList.component.scss"
const fetchIconsPage = (
limit: number,
from: number,
filters: ObjectType,
dataSetter: FunctionType,
pageSetter: FunctionType,
setLoading: FunctionType,
) => {
const pageApi =
API_CONFIG.STATIC_HOST[process.env.NODE_ENV] +
ENDPOINTS.STATIC.ICON.ROOT +
ENDPOINTS.STATIC.ICON.PAGE
const queryParams: ObjectType = { pageSize: limit, from, ...filters }
get(pageApi, queryParams)
.then((response) => {
if (response && response.status === 200) {
if (response.body && dataSetter) {
dataSetter(response.body)
pageSetter(response.body.slice(0, 100))
}
}
setLoading(false)
})
.catch((error) => {
console.error(error)
setLoading(false)
})
}
const IconsList = (props: IconsListProps): JSX.Element => {
// const { variant } = props
const [searchText, setSearchText] = useState<string | undefined>()
const [icons, setIcons] = useState<Array<IconResponse>>()
const [page, setPage] = useState<Array<IconResponse>>()
const [view, setView] = useState<SegmentData>()
const [loading, setLoading] = useState<boolean>()
const [isSelectMode, toggleSelectMode] = useState<boolean>()
const [selectedIcons, setSelectedIcons] = useState<Array<IconResponse>>()
const { setPanelContent: setRightPanelContent } = usePanelContent(false)
const { notify } = useNotification()
const dispatch = useDispatch()
const favorites = useSelector(getFavorites)
const selectedTag = useSelector(getSelectedTag) as string | undefined
const iconStyles = useSelector(getIconStyles) as IconStyles | undefined
const navigate = useNavigate()
useEffect(() => {
setView({ name: ArIconTileTypes.COMFY })
}, [])
useEffect(() => {
if (favorites) {
setRightPanelContent({
componentName: favorites.length > 0 ? "FavoritesList" : "",
})
}
}, [favorites, setRightPanelContent])
useEffect(() => {
const filters: ObjectType = {}
if (selectedTag) {
filters.tags = selectedTag
}
if (searchText) {
filters.search = searchText
}
setLoading(true)
fetchIconsPage(2000, 0, filters, setIcons, setPage, setLoading)
}, [selectedTag, searchText])
const onIconTileClick = (iconProps: IconResponse) => {
isSelectMode
? setSelectedIcons((currentIcons) => {
currentIcons = [...(currentIcons || [])]
const existingIconIndex = currentIcons?.findIndex(
(currentIcon) => currentIcon.name === iconProps.name,
)
if (existingIconIndex > -1) {
currentIcons.splice(existingIconIndex, 1)
} else {
currentIcons.push(iconProps)
}
return currentIcons
})
: view?.name === ArIconTileTypes.COMPACT &&
notify({
show: true,
message: `Icon link for ${iconProps.name} copied`,
uid: uuid(),
})
}
return (
<div className="ar-IconsList h-100 w-100 overflow-auto position-relative d-flex flex-column">
{!icons && (
<Loader label="Loading Icons..." type={ArLoaderTypes.SHAPES} />
)}
<div className="ar-IconsList__header-pagination-search-upload py-2 px-3 mb-2 border">
<div className="row">
<div className="col d-none d-md-flex align-items-center">
<h6 className="mb-0 h-100 flex-center px-3 border-right">
<MenuButton
buttonProps={{
variant: ArButtonVariants.LINK,
content: "Categories",
size: ArSizes.SMALL,
}}
splitOptions={[
{
label: "All",
},
{
label: "Business",
},
{
label: "Medical",
},
]}
/>
</h6>
</div>
<span className="col flex-v-center justify-content-end">
<Popover version="v2">
<Icon
icon="io/IoIosColorPalette"
slot={ArPopoverSlots.ANCHOR}
attributes={{
classes:
"ar-IconsList__color-palette cursor-pointer ms-auto hover-shadow me-3",
colors: {
fillColor: "orange",
},
size: "2rem",
}}
events={{ onClick: () => { } }}
/>
<IconStyleSelector
slot={ArPopoverSlots.POPOVER}
setIconStyles={(iconStylesUpdater) => {
if (typeof iconStylesUpdater === "function") {
dispatch(setIconStyles(iconStylesUpdater(iconStyles)))
} else {
dispatch(setIconStyles(iconStylesUpdater))
}
}}
layout="horizontal"
/>
</Popover>
<Button
classes="h-100 float-end me-3"
content="Create"
size={ArSizes.SMALL}
variant={ArButtonVariants.LINKHOVEREFFECT}
preIcon="io5/IoCreateOutline"
onClick={() => toggleSelectMode(true)}
/>
<Button
classes="ar-IconsList__upload-button h-100 float-end"
content="Upload"
size={ArSizes.SMALL}
variant={ArButtonVariants.PRIMARY}
/>
<SegmentedControl
classes="d-none d-sm-inline ms-3"
hasUniformSegments
segments={[
{
name: "comfy",
icon: "md/MdViewComfy",
tooltip: "View Comfortable",
},
{
name: "compact",
icon: "md/MdViewCompact",
tooltip: "View Compact",
},
{
name: "list",
icon: "io5/IoList",
tooltip: "Quick Peak",
},
]}
onChange={(view) => setView(view as SegmentData)}
/>
</span>
<div className="col d-none d-md-flex flex-v-center">
<SearchField
classes="bg-white"
placeholder="Search by name, tags, description"
onChange={debounce(
(event: ChangeEvent<HTMLInputElement>) =>
setSearchText(event.target.value),
1000,
)}
items={
icons
? icons.map(
(icon): SearchItem => ({
label: icon.name,
data: icon,
}),
)
: []
}
hidePopup
/>
</div>
</div>
</div>
<div
className={`ar-IconList__selection-manager d-flex${isSelectMode ? " mb-2 py-2 px-3 border" : " hide"
}`}
>
{selectedIcons && selectedIcons.length > 0 && (
<Pillbox
data={(selectedIcons || []).map((icon) => ({
label: icon.name,
deletable: true,
}))}
onChange={() => { }}
/>
)}
<div className="ms-auto d-flex">
<Button
classes="me-3"
content="Cancel"
variant={ArButtonVariants.SECONDARY}
size={ArSizes.SMALL}
onClick={() => {
toggleSelectMode(false)
setSelectedIcons(undefined)
}}
/>
<Button
content="Done"
variant={ArButtonVariants.PRIMARY}
size={ArSizes.SMALL}
onClick={() =>
navigate("/icons/merge-icons", { state: selectedIcons })
}
disabled={!selectedIcons || selectedIcons.length === 0}
/>
</div>
</div>
{page && (
<div className="ar-IconsList__icon-tile-container-wrapper d-flex flex-grow-1 justify-content-between mh-100 overflow-auto">
<div
className="ar-IconsList__icon-tile-container py-2 px-3 border d-grid w-100"
style={{
gridTemplateColumns: "repeat(10, minmax(8rem, auto))",
gridAutoRows: "max(8rem)",
}}
>
{loading && (
<Loader label="Applying filters..." type={ArLoaderTypes.CIRCLE} />
)}
{page.map((icon, index) => (
<Popover
trigger={ArPopoverTriggers.HOVER}
key={"icon-tile-popover-" + index}
classes="mb-3"
version="v2"
>
{!view || view.name !== ArIconTileTypes.LIST ? (
<span slot={ArPopoverSlots.POPOVER}>
<b className="d-block">{icon.group}</b>
{icon.name
?.match(/[A-Z][a-z]+/g)
?.slice(1)
.join(" ")}
</span>
) : null}
<IconTile
type={
view && view.name
? (view.name as ArIconTileTypes)
: ArIconTileTypes.COMFY
}
hideBorder={view?.name !== ArIconTileTypes.LIST}
classes={view?.name}
slot={ArPopoverSlots.ANCHOR}
key={icon + "-" + index}
icon={icon}
iconSize={
view?.name === ArIconTileTypes.LIST ? "1rem" : "2rem"
}
onClick={onIconTileClick}
tools={[
{
iconProps: {
icon: "sl.SlOptions",
attributes: {
colors: {
fillColor: "white",
hoverFillColor: "lightblue",
},
},
},
children: [
{
iconProps: {
icon: "io.IoIosShareAlt",
attributes: {
colors: { hoverFillColor: "lightblue" },
},
},
name: "share",
onClick: () => { },
},
],
name: "more-options",
onClick: () => { },
},
{
iconProps: {
icon: "ai/AiFillLike",
attributes: {
colors: {
fillColor: "white",
hoverFillColor: "lightblue",
},
},
},
name: "like",
onClick: () => { },
},
{
iconProps: {
icon: "md/MdFavorite",
attributes: {
colors: {
fillColor: "white",
hoverFillColor: "lightblue",
toggleFillColor: "red",
},
},
toggled: false,
},
name: "favorite",
onClick: () => {
const favorite = favorites.find(
(searchedIcon: IconTileProps) =>
icon.name === searchedIcon.icon.name,
)
if (!favorites || !favorite) {
dispatch(setFavorites({ icon }))
notify({
message: "Icon Added to your favorites",
show: true,
uid: uuid(),
})
} else {
dispatch(removeFavorite({ icon }))
notify({
message: "Icon removed from your favorites",
show: true,
uid: uuid(),
})
}
},
},
]}
hideFooter={
(view && (view.name as ArIconTileTypes)) !==
ArIconTileTypes.LIST
}
selectable={isSelectMode}
fillColor={iconStyles?.fillColor}
strokeColor={iconStyles?.strokeColor}
strokeWidth={iconStyles?.strokeWidth}
/>
</Popover>
))}
</div>
</div>
)}
{/* {slices && ( */}
<Pagination
classes="my-3 flex-center"
data={icons}
maxPillsToShow={5}
pageSetter={setPage}
// trigger={ArPageTriggers.SCROLL}
count={1}
load={100}
dataFetcher={(load, count) =>
fetchIconsPage(load, count, {}, setIcons, setPage, setLoading)
}
/>
{/* )} */}
</div>
)
}
export default IconsList

20
src/IconsMerge.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { useLocation } from "react-router-dom"
import { usePanelContent } from "@armco/utils/hooks"
import { Main } from "@armco/shared-components"
import IconMergeContainer from "./IconMergeContainer"
const IconsMerge = (): JSX.Element => {
const { panelContent: rightPanelContent } = usePanelContent(false)
const location = useLocation()
return (
<div className="ar-IconsMerge">
<Main
mainContent={<IconMergeContainer icons={location.state} />}
rightPanelContent={rightPanelContent}
/>
</div>
)
}
export default IconsMerge

64
src/Test.tsx Normal file
View File

@@ -0,0 +1,64 @@
import React, { useEffect } from "react"
import ReactDOM from "react-dom/client"
import {
BrowserRouter,
Navigate,
RouteObject,
useRoutes,
} from "react-router-dom"
import { Provider } from "react-redux"
import { ArProvider } from "@armco/utils/providers"
import { useTheme } from "@armco/utils/hooks"
import { store } from "./store"
import Icons from "./Icons"
import IconsMerge from "./IconsMerge"
import IconInfo from "./IconInfo"
import Clusters from "./Clusters"
import { ArThemes } from "./types"
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
const routes: Array<RouteObject & { class?: string }> = [
{
path: "/",
class: "icons",
element: <Navigate to="/icons" />,
},
{
path: "/icons",
class: "icons",
element: <Icons />,
},
{
path: "/icons/merge-icons",
class: "merge-icons",
element: <IconsMerge />,
},
{
path: "/icon/:group/:name",
class: "icon",
element: <IconInfo />,
},
{
path: "/clusters",
class: "clusters",
element: <Clusters />,
},
]
const Router = () => {
const { setTheme } = useTheme()
useEffect(() => setTheme(ArThemes.LIGHT1), [setTheme])
return useRoutes(routes)
}
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<ArProvider>
<Router />
</ArProvider>
</Provider>
</BrowserRouter>
</React.StrictMode>,
)

46
src/helper.ts Normal file
View File

@@ -0,0 +1,46 @@
import { IconProps } from "./types"
import { applyStyles } from "@armco/icon/helper"
const parser = new DOMParser()
export function downloadSvg(
svgString: string,
svgName: string,
svgProps?: IconProps,
) {
const svgElement = parser
.parseFromString(svgString, "image/svg+xml")
.querySelector("svg")
if (svgElement) {
applyStyles(svgElement, { ...(svgProps || {}), fillPath: true })
svgString = svgElement.outerHTML
}
const base64doc = btoa(unescape(encodeURIComponent(svgString)))
const a = document.createElement("a")
const e = new MouseEvent("click")
a.download = svgName + ".svg"
a.href = "data:image/svg+xml;base64," + base64doc
a.dispatchEvent(e)
}
export const complement = (hex?: string) => {
if (hex) {
const hexParts = hex.substring(1).toLowerCase().split("")
const hexMap = { a: 10, b: 11, c: 12, d: 13, e: 14, f: 15 }
const reverseMap = { 10: "a", 11: "b", 12: "c", 13: "d", 14: "e", 15: "f" }
let complementValue = "#"
hexParts.forEach((part: string) => {
let num = +part
if (isNaN(num)) {
num = hexMap[part as keyof object]
}
const complementNumeric = 15 - num
complementValue +=
complementNumeric > 9
? reverseMap[complementNumeric as keyof object]
: complementNumeric
})
return complementValue
}
return "#fff"
}

4
src/index.tsx Normal file
View File

@@ -0,0 +1,4 @@
export { default as Clusters } from "./Clusters"
export { default as IconInfo } from "./IconInfo"
export { default as IconsMerge } from "./IconsMerge"
export { default } from "./Icons"

71
src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1,71 @@
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: "development" | "production"
readonly PUBLIC_URL: string
}
}
declare module "*.avif" {
const src: string
export default src
}
declare module "*.bmp" {
const src: string
export default src
}
declare module "*.gif" {
const src: string
export default src
}
declare module "*.jpg" {
const src: string
export default src
}
declare module "*.jpeg" {
const src: string
export default src
}
declare module "*.png" {
const src: string
export default src
}
declare module "*.webp" {
const src: string
export default src
}
declare module "*.svg" {
import * as React from "react"
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>
const src: string
export default src
}
declare module "*.module.css" {
const classes: { readonly [key: string]: string }
export default classes
}
declare module "*.module.scss" {
const classes: { readonly [key: string]: string }
export default classes
}
declare module "*.module.sass" {
const classes: { readonly [key: string]: string }
export default classes
}

13
src/store.ts Normal file
View File

@@ -0,0 +1,13 @@
import { configureStore } from "@reduxjs/toolkit"
import icons from "./Icons.slice"
import iconInfo from "./IconInfo.slice"
export const store = configureStore({
reducer: {
icons,
iconInfo,
},
devTools: {
name: "icon-spot", // Specify the name of your store for Redux DevTools
},
})

105
src/types.ts Normal file
View File

@@ -0,0 +1,105 @@
import { MouseEventHandler, ReactNode, Dispatch, SetStateAction } from "react"
import { IconProps, IconResponse as BaseIconResponse, IconStyles } from "@armco/icon"
export type { IconProps, IconStyles } from "@armco/icon"
export type { ObjectType, FunctionType, ArrayType } from "@armco/types"
export {
ArButtonVariants,
ArLoaderTypes,
ArPopoverPositions,
ArPopoverSlots,
ArPopoverTriggers,
ArSizes,
RecusionConditionTypes,
} from "@armco/shared-components/enums"
export type {
FilterState,
SearchItem,
SegmentData,
} from "@armco/shared-components/entity"
export {
ArIconSourceTypes,
ArIconTileTypes,
} from "@armco/icon/enums"
// ArThemes - defined locally since @armco/utils/enums path may not be accessible
export enum ArThemes {
LIGHT1 = "th-light-1",
DARK1 = "th-dark-1",
}
// Extended IconResponse with additional fields used in icon-spot
export interface IconResponse extends BaseIconResponse {
createdby?: string
meta?: {
downloadedTimes?: number
likedTimes?: number
size?: number
[key: string]: unknown
}
[key: string]: unknown // Allow index access for ObjectType compatibility
}
// ─────────────────────────────────────────────────────────────
// icon-spot local component prop types
// ─────────────────────────────────────────────────────────────
export interface IconTileProps {
classes?: string
fillColor?: string
hideBorder?: boolean
hideFooter?: boolean
icon: IconResponse
iconSize?: string
onClick?: (icon: IconResponse) => void
selectable?: boolean
slot?: string
strokeColor?: string
strokeWidth?: string
tools?: Array<ToolItemConfig>
toolsPlacement?: string
type?: string
}
export interface ToolItemConfig {
children?: Array<ToolItemConfig>
iconProps: IconProps
onClick: MouseEventHandler<HTMLImageElement>
name: string
tooltip?: string
}
export interface FavoritesItemProps {
index: number
favorite: IconTileProps
}
export interface FavoritesListProps {
classes?: string
}
export interface IconControllerProps {
group?: string
icon?: IconResponse
name?: string
}
export interface IconsListProps {
filters?: Record<string, unknown>
tags?: Record<string, unknown>
}
export interface IconStyleSelectorProps {
iconStyles?: IconStyles
layout?: "horizontal" | "vertical"
setIconStyles: Dispatch<SetStateAction<IconStyles | undefined>>
slot?: string
}
export interface IconEditorProps {
layout?: "horizontal" | "vertical"
}
export interface IconMergeContainerProps {
icons?: Array<IconResponse>
}

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

42
vite-dev.config.ts Normal file
View File

@@ -0,0 +1,42 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), libInjectCss(), dts({ outDir: "build/types" })],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|Test).{ts,tsx}")),
},
sourcemap: true,
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})

26
vite-run.config.ts Normal file
View File

@@ -0,0 +1,26 @@
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import { config } from "dotenv"
config()
// https://vitejs.dev/config/
export default defineConfig({
define: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
},
plugins: [react()],
server: {
open: true,
},
build: {
outDir: "build",
sourcemap: true,
},
test: {
globals: true,
environment: "jsdom",
setupFiles: "src/setupTests",
mockReset: true,
},
})

45
vite.config.ts Normal file
View File

@@ -0,0 +1,45 @@
import { resolve } from "path"
import { glob } from "glob"
import { defineConfig } from "vitest/config"
import react from "@vitejs/plugin-react"
import dts from "vite-plugin-dts"
import { libInjectCss } from "vite-plugin-lib-inject-css"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
libInjectCss(),
dts({ outDir: "build/types", exclude: ["**/Test.tsx", "**/*.test.tsx"] }),
],
build: {
outDir: "build",
lib: {
entry: glob.sync(resolve(__dirname, "src/**/!(*.d|Test).{ts,tsx}")),
},
rollupOptions: {
treeshake: true,
external: [
new RegExp("react*"),
new RegExp("highcharts*"),
new RegExp("@armco/*"),
"d3",
"uuid",
],
output: [
{
format: "es",
dir: "build/es",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
{
format: "cjs",
dir: "build/cjs",
entryFileNames: "[name].js",
chunkFileNames: "[name]-chunk.js",
},
],
},
},
})