@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 +13 -0
- package/e2e/descope-icon.spec.ts +26 -0
- package/package.json +33 -0
- package/project.json +17 -0
- package/src/component/IconClass.js +98 -0
- package/src/component/helpers.js +51 -0
- package/src/component/index.js +5 -0
- package/src/theme.js +8 -0
- package/stories/descope-icon.stories.js +37 -0
- package/stories/google.svg +25 -0
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
|
+
};
|
package/src/theme.js
ADDED
|
@@ -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>
|