Skip to content

Commit

Permalink
feat(icon): initial icon implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
eTallang committed Dec 4, 2024
1 parent c906203 commit f6d8385
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 4 deletions.
Binary file modified bun.lockb
Binary file not shown.
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,24 @@
},
"dependencies": {
"@lit/react": "^1.0.6",
"@storybook/addon-themes": "^8.4.6",
"lit": "^3.2.1"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "^3.2.2",
"@computas/designsystem-breadcrumbs": "workspace:*",
"@computas/designsystem-icon": "workspace:*",
"@storybook/addon-essentials": "^8.4.6",
"@storybook/addon-themes": "^8.4.6",
"@storybook/blocks": "^8.4.6",
"@storybook/manager-api": "^8.4.6",
"@storybook/test": "^8.4.6",
"@storybook/theming": "^8.4.6",
"@storybook/web-components": "^8.4.6",
"@storybook/web-components-vite": "^8.4.6",
"@types/node": "^22.9.0",
"@types/node": "^22.10.1",
"storybook": "^8.4.6",
"typescript": "~5.7.0",
"vite": "^6.0.0"
"typescript": "~5.7.2",
"vite": "^6.0.2"
}
}
73 changes: 73 additions & 0 deletions src/components/icon/icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import DOMPurify from 'dompurify';
import { LitElement, css, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';

import { getIcon } from './store';

type IconSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';

@customElement('cx-icon')
export class Icon extends LitElement {
static readonly styles = css`
:host {
--_icon-size: 32px;
display: inline-block;
width: var(--_icon-size);
height: var(--_icon-size);
line-height: 0;
}
:host([size='xxs']) {
--_icon-size: 8px;
}
:host([size='xs']) {
--_icon-size: 16px;
}
:host([size='sm']) {
--_icon-size: 24px;
}
:host([size='md']) {
--_icon-size: 32px;
}
:host([size='lg']) {
--_icon-size: 40px;
}
:host([size='xl']) {
--_icon-size: 48px;
}
:host([size='xxl']) {
--_icon-size: 56px;
}
svg {
width: 100%;
height: 100%;
}
`;

@property({ type: String, reflect: true })
name = '';

// Setting this to reflect: true will make the size attribute reflect to the DOM, which makes the current
// CSS-selector-based approach for sizing work in React. If another approach is used, this whole property may be removed.
@property({ type: String, reflect: true })
size: IconSize | undefined = undefined;

render() {
return html`${unsafeHTML(DOMPurify.sanitize(getIcon(this.name)?.svg ?? ''))}`;
}
}

declare global {
interface HTMLElementTagNameMap {
'cx-icon': Icon;
}
}
2 changes: 2 additions & 0 deletions src/components/icon/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './icon';
export { addIcons } from './store';
12 changes: 12 additions & 0 deletions src/components/icon/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@computas/designsystem-icon",
"version": "1.0.0",
"type": "module",
"private": "true",
"scripts": {
"build": "tsup"
},
"devDependencies": {
"tsup": "^8.3.5"
}
}
10 changes: 10 additions & 0 deletions src/components/icon/react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createComponent } from '@lit/react';
import * as React from 'react';

import { Icon } from './index';

export const CxIcon = createComponent({
tagName: 'cx-icon',
elementClass: Icon,
react: React,
});
40 changes: 40 additions & 0 deletions src/components/icon/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
interface IconData {
svg: string;
}
type IconRegistry = Record<string, IconData>;

let _cxGlobalIconsStore: IconRegistry = {};

/**
* Add icons to the global icon store to make them available for use in the `<e-icon>/Icon` component.
*
* You can add multiple icons at once. The key of the input object is the icon name corresponding to the `name`
* prop in the `<e-icon>/Icon` component. While this can be any string, it is recommended to use the icon name as the key.
* Some selectors depend on the `filledColor` icons to keep `filledColor` in the icon name to work properly.
* @param icons - An object of icon names and their corresponding SVG data.
*
* @example
* ```ts
* import { addIcons } from '@computas/designsystem/icon';
*
* // import the icon data (SVG string)
* import download from '@computas/designsystem/icon/svg/download?raw';
*
* addIcons({
* download: { svg: download }
* });
* ```
*/
export const addIcons = (icons: IconRegistry) => {
_cxGlobalIconsStore = { ..._cxGlobalIconsStore, ...icons };
};

export const getIcon = (name: string): IconData | undefined => {
// Need to check `name` because it can be `undefined` on initial render depending on property vs attribute
if (name && !_cxGlobalIconsStore[name]) {
throw new Error(
`Icon "${name}" not found. Ensure the icon name is correct and that it has been added to the global icon store using \`addIcons\`.`,
);
}
return _cxGlobalIconsStore[name];
};

0 comments on commit f6d8385

Please sign in to comment.