@descope-ui/descope-icon 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
+
5
+ ## 0.0.1 (2025-03-04)
6
+
7
+ ### Dependency Updates
8
+
9
+ * `e2e-utils` updated to version `0.0.1`
10
+ * `test-assets` updated to version `0.0.1`
11
+ * `@descope-ui/common` updated to version `0.0.7`
12
+ * `@descope-ui/theme-globals` updated to version `0.0.7`
13
+ # Changelog
@@ -0,0 +1,26 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { getStoryUrl, loopPresets } from 'e2e-utils';
3
+
4
+ const storyName = 'descope-icon';
5
+ const componentName = 'descope-icon';
6
+
7
+ const presets = {
8
+ 'from url': { icon: 'fromUrl' },
9
+ 'from url with fill-color': { icon: 'fromUrl', 'fill-color': 'true', 'st-fill': 'red' },
10
+ svg64: { icon: 'base64svg' },
11
+ 'svg64 with fill-color': { icon: 'base64svg', 'fill-color': 'true', 'st-fill': 'green' },
12
+ png64: { icon: 'base64png' },
13
+ };
14
+
15
+ test.describe('theme', () => {
16
+ loopPresets(presets, (preset, name) => {
17
+ test(name, async ({ page }) => {
18
+ await page.goto(getStoryUrl(storyName, preset));
19
+ await page.waitForSelector(componentName);
20
+ const component = page.locator(componentName);
21
+ expect(
22
+ await component.screenshot({ animations: 'disabled', timeout: 3000, caret: 'hide' })
23
+ ).toMatchSnapshot();
24
+ });
25
+ });
26
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@descope-ui/descope-icon",
3
+ "version": "0.0.1",
4
+ "exports": {
5
+ ".": {
6
+ "import": "./src/component/index.js"
7
+ },
8
+ "./theme": {
9
+ "import": "./src/theme.js"
10
+ },
11
+ "./class": {
12
+ "import": "./src/component/IconClass.js"
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "@playwright/test": "1.38.1",
17
+ "e2e-utils": "0.0.1",
18
+ "test-assets": "0.0.1"
19
+ },
20
+ "dependencies": {
21
+ "@vaadin/icon": "24.3.4",
22
+ "dompurify": "^3.1.6",
23
+ "@descope-ui/common": "0.0.7",
24
+ "@descope-ui/theme-globals": "0.0.7"
25
+ },
26
+ "publishConfig": {
27
+ "link-workspace-packages": false
28
+ },
29
+ "scripts": {
30
+ "test": "echo 'No tests defined' && exit 0",
31
+ "test:e2e": "echo 'No e2e tests defined' && exit 0"
32
+ }
33
+ }
package/project.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@descope-ui/descope-icon",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/web-components/components/descope-icon/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "version": {
8
+ "executor": "@jscutlery/semver:version",
9
+ "options": {
10
+ "trackDeps": true,
11
+ "push": false,
12
+ "preset": "conventional"
13
+ }
14
+ }
15
+ },
16
+ "tags": []
17
+ }
@@ -0,0 +1,98 @@
1
+ /* eslint-disable no-use-before-define */
2
+ import { createStyleMixin, draggableMixin, componentNameValidationMixin } from '@descope-ui/common/components-mixins';
3
+ import { createBaseClass } from '@descope-ui/common/base-classes';
4
+ import { compose } from '@descope-ui/common/utils';
5
+ import { getComponentName } from '@descope-ui/common/components-helpers';
6
+ import { createIcon } from './helpers';
7
+ import { injectStyle } from '@descope-ui/common/components-helpers';
8
+
9
+ export const componentName = getComponentName('icon');
10
+
11
+ class RawIcon extends createBaseClass({ componentName, baseSelector: 'slot' }) {
12
+ static get observedAttributes() {
13
+ return ['src'];
14
+ }
15
+
16
+ #icon;
17
+
18
+ constructor() {
19
+ super();
20
+
21
+ this.attachShadow({ mode: 'open' }).innerHTML = `
22
+ <slot></slot>
23
+ `;
24
+
25
+ injectStyle(
26
+ `
27
+ :host > slot {
28
+ box-sizing: border-box;
29
+ width: 100%;
30
+ height: 100%;
31
+ display: flex;
32
+ overflow: hidden;
33
+ }
34
+ :host {
35
+ display: inline-block;
36
+ }
37
+ `,
38
+ this
39
+ );
40
+ }
41
+
42
+ init() {
43
+ super.init?.();
44
+ this.toggleVisibility(this.src);
45
+ }
46
+
47
+ toggleVisibility(isVisible) {
48
+ this.style.display = isVisible ? '' : 'none';
49
+ }
50
+
51
+ get src() {
52
+ return this.getAttribute('src');
53
+ }
54
+
55
+ // in order to fill an SVG with `currentColor` override all of its `fill` and `path` nodes
56
+ // with the value from the `st-fill` attribute
57
+ // eslint-disable-next-line class-methods-use-this
58
+ updateFillColor(node) {
59
+ // set fill to root node and all its relevant selectors
60
+ const elementsToReplace = [node, ...node.querySelectorAll('*[fill]')];
61
+
62
+ elementsToReplace.forEach((ele) => {
63
+ ele.setAttribute(
64
+ 'fill',
65
+ `var(${IconClass.cssVarList.fill}, ${ele.getAttribute('fill') || "''"})`
66
+ );
67
+ });
68
+ }
69
+
70
+ attributeChangedCallback(attrName, oldValue, newValue) {
71
+ super.attributeChangedCallback?.(attrName, oldValue, newValue);
72
+
73
+ if (oldValue === newValue) return;
74
+
75
+ if (attrName === 'src') {
76
+ this.toggleVisibility(newValue);
77
+
78
+ createIcon(this.src).then((res) => {
79
+ this.innerHTML = '';
80
+ if (res) {
81
+ const clonedNode = res.cloneNode(true);
82
+ this.updateFillColor(clonedNode);
83
+ this.appendChild(clonedNode);
84
+ }
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ export const IconClass = compose(
91
+ createStyleMixin({
92
+ mappings: {
93
+ fill: {},
94
+ },
95
+ }),
96
+ draggableMixin,
97
+ componentNameValidationMixin
98
+ )(RawIcon);
@@ -0,0 +1,51 @@
1
+ import DOMPurify from 'dompurify';
2
+
3
+ const getFileExtension = (path) => {
4
+ const match = path.match(/\.([0-9a-z]+)(?:[\\?#]|$)/i);
5
+ return match ? match[1] : null;
6
+ };
7
+
8
+ const base64Prefix = 'data:image/svg+xml;base64,';
9
+
10
+ const isBase64Svg = (src) => src.startsWith(base64Prefix);
11
+
12
+ const createImgEle = (src) => {
13
+ const ele = document.createElement('img');
14
+ ele.setAttribute('src', src);
15
+ return ele;
16
+ };
17
+
18
+ const createSvgEle = (text) => {
19
+ // we want to purify the SVG to avoid XSS attacks
20
+ const clean = DOMPurify.sanitize(text, { USE_PROFILES: { svg: true, svgFilters: true } });
21
+
22
+ const parser = new DOMParser();
23
+ const ele = parser.parseFromString(clean, 'image/svg+xml').querySelector('svg');
24
+ return ele;
25
+ };
26
+
27
+ export const createIcon = async (src) => {
28
+ try {
29
+ let ele;
30
+ if (isBase64Svg(src)) {
31
+ // handle base64 source
32
+ const svgXml = atob(src.slice(base64Prefix.length));
33
+ ele = createSvgEle(svgXml);
34
+ } else if (getFileExtension(src) === 'svg') {
35
+ // handle urls
36
+ const fetchedSrc = await fetch(src);
37
+ const text = await fetchedSrc.text();
38
+ ele = createSvgEle(text);
39
+ } else {
40
+ // handle binary
41
+ ele = createImgEle(src);
42
+ }
43
+
44
+ ele.style.setProperty('max-width', '100%');
45
+ ele.style.setProperty('max-height', '100%');
46
+
47
+ return ele;
48
+ } catch {
49
+ return null;
50
+ }
51
+ };
@@ -0,0 +1,5 @@
1
+ import { componentName, IconClass } from './IconClass';
2
+
3
+ customElements.define(componentName, IconClass);
4
+
5
+ export { IconClass, componentName };
package/src/theme.js ADDED
@@ -0,0 +1,8 @@
1
+ import { IconClass } from './component/IconClass';
2
+
3
+ export const vars = IconClass.cssVarList;
4
+
5
+ const icon = {};
6
+
7
+ export default icon;
8
+
@@ -0,0 +1,37 @@
1
+ import { componentName } from '../src/component';
2
+ import { fillControl } from '@descope-ui/common/sb-controls';
3
+ import fromUrl from './google.svg?no-inline';
4
+ import {base64png, base64svg} from 'test-assets';
5
+
6
+ const icons = {
7
+ fromUrl,
8
+ base64png,
9
+ base64svg,
10
+ };
11
+
12
+ const Template = ({ icon, 'st-fill': fill }) => {
13
+ return `
14
+ <descope-icon
15
+ src="${icons[icon]}"
16
+ ${fill ? `st-fill="${fill}"` : ''}
17
+ ></descope-icon>
18
+ `;
19
+ };
20
+
21
+ export default {
22
+ component: componentName,
23
+ title: 'descope-icon',
24
+ argTypes: {
25
+ ...fillControl,
26
+ icon: {
27
+ options: ['base64svg', 'base64png', 'fromUrl'],
28
+ control: { type: 'select' },
29
+ },
30
+ },
31
+ };
32
+
33
+ export const Default = Template.bind({});
34
+
35
+ Default.args = {
36
+ icon: 'fromUrl',
37
+ };
@@ -0,0 +1,25 @@
1
+ <svg
2
+ width="1.5em"
3
+ height="1.5em"
4
+ viewBox="0 0 20 20"
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ data-icon="google"
7
+ >
8
+ <path
9
+ d="m19.6 10.2274c0-.70912-.0636-1.39092-.1818-2.04552h-9.4182v3.86822h5.3818c-.2318 1.25-.9363 2.3091-1.9954 3.0182v2.5091h3.2318c1.8909-1.7409 2.9818-4.3046 2.9818-7.35z"
10
+ fill="#4285f4"
11
+ />
12
+ <path
13
+ d="m10 19.9999c2.7 0 4.9636-.8955 6.6181-2.4227l-3.2318-2.5091c-.8954.6-2.0409.9545-3.3863.9545-2.6046 0-4.8091-1.7591-5.5955-4.1227h-3.3409v2.5909c1.6455 3.2682 5.0273 5.5091 8.9364 5.5091z"
14
+ fill="#34a853"
15
+ />
16
+ <path
17
+ d="m4.4045 11.8999c-.2-.6-.3136-1.2409-.3136-1.89997 0-.6591.1136-1.3.3136-1.9v-2.5909h-3.3409c-.6772 1.35-1.0636 2.8773-1.0636 4.4909 0 1.61357.3864 3.14087 1.0636 4.49087z"
18
+ fill="#fbbc04"
19
+ />
20
+ <path
21
+ d="m10 3.9773c1.4681 0 2.7863.5045 3.8227 1.4954l2.8682-2.8682c-1.7318-1.6136-3.9955-2.6045-6.6909-2.6045-3.9091 0-7.2909 2.2409-8.9364 5.5091l3.3409 2.5909c.7864-2.3636 2.9909-4.1227 5.5955-4.1227z"
22
+ fill="#e94235"
23
+ />
24
+ <script>alert(1)</script>
25
+ </svg>