Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

vanilla-colorful

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

vanilla-colorful - npm Package Compare versions

Comparing version 0.3.1 to 0.4.0

lib/utils/clamp.d.ts

16

CHANGELOG.md

@@ -5,2 +5,18 @@ # Changelog

## [0.4.0](https://github.com/web-padawan/vanilla-colorful/compare/v0.3.1...v0.4.0) (2020-09-28)
### ⚠ BREAKING CHANGES
* rename protected methods
### Features
* implement accessibility support ([23805f5](https://github.com/web-padawan/vanilla-colorful/commit/23805f5789468dc4682d042ec53c4c37144ca831))
### Internal Changes
* hide internals with symbols ([0f1d7e8](https://github.com/web-padawan/vanilla-colorful/commit/0f1d7e8849802ff3a41d79d12b02a229bfa8c574))
### [0.3.1](https://github.com/web-padawan/vanilla-colorful/compare/v0.3.0...v0.3.1) (2020-09-19)

@@ -7,0 +23,0 @@

8

lib/components/alpha-color-picker.d.ts

@@ -1,9 +0,11 @@

import { ColorPicker } from './color-picker.js';
import { ColorPicker, $render } from './color-picker.js';
import type { AnyColor, HsvaColor } from '../types';
import './alpha.js';
declare const $a: unique symbol;
export declare abstract class AlphaColorPicker<C extends AnyColor> extends ColorPicker<C> {
private _a;
private [$a];
constructor();
protected _render(hsva: HsvaColor): void;
protected [$render](hsva: HsvaColor): void;
}
export {};
//# sourceMappingURL=alpha-color-picker.d.ts.map

@@ -1,15 +0,16 @@

import { ColorPicker } from './color-picker.js';
import { ColorPicker, $render } from './color-picker.js';
import { createTemplate, createRoot } from '../utils/dom.js';
import './alpha.js';
const tpl = createTemplate('<vc-alpha part="alpha" exportparts="pointer: alpha-pointer"></vc-alpha>');
const $a = Symbol('a');
export class AlphaColorPicker extends ColorPicker {
constructor() {
super();
this._a = createRoot(this, tpl).lastElementChild;
this[$a] = createRoot(this, tpl).lastElementChild;
}
_render(hsva) {
super._render(hsva);
this._a.hsva = hsva;
[$render](hsva) {
super[$render](hsva);
this[$a].hsva = hsva;
}
}
//# sourceMappingURL=alpha-color-picker.js.map

@@ -7,5 +7,8 @@ import { Interactive, Interaction } from './interactive.js';

connectedCallback(): void;
private _hsva;
get xy(): boolean;
get hsva(): HsvaColor;
set hsva(hsva: HsvaColor);
getMove(interaction: Interaction): Record<string, number>;
getMove(interaction: Interaction, key?: boolean): Record<string, number>;
}
//# sourceMappingURL=alpha.d.ts.map
import { Interactive } from './interactive.js';
import { hsvaToHslaString } from '../utils/convert.js';
import { createTemplate, createRoot } from '../utils/dom.js';
import { clamp } from '../utils/clamp.js';
import styles from '../styles/alpha.js';

@@ -10,2 +11,5 @@ const template = createTemplate(`<style>${styles}</style><div id="gradient"></div>`);

this.gradient = createRoot(this, template).querySelector('#gradient');
this.setAttribute('aria-label', 'Alpha');
this.setAttribute('aria-valuemin', '0');
this.setAttribute('aria-valuemax', '1');
}

@@ -19,15 +23,26 @@ connectedCallback() {

}
get xy() {
return false;
}
get hsva() {
return this._hsva;
}
set hsva(hsva) {
this._hsva = hsva;
const colorFrom = hsvaToHslaString({ ...hsva, a: 0 });
const colorTo = hsvaToHslaString({ ...hsva, a: 1 });
const value = hsva.a * 100;
this.gradient.style.backgroundImage = `linear-gradient(to right, ${colorFrom}, ${colorTo}`;
this.setStyles({
top: '50%',
left: `${hsva.a * 100}%`,
left: `${value}%`,
color: hsvaToHslaString(hsva)
});
const round = Math.round(value);
this.setAttribute('aria-valuenow', `${round}`);
this.setAttribute('aria-valuetext', `${round}%`);
}
getMove(interaction) {
getMove(interaction, key) {
// Alpha always fit into [0, 1] range
return { a: interaction.left };
return { a: key ? clamp(this.hsva.a + interaction.left) : interaction.left };
}

@@ -34,0 +49,0 @@ }

import type { AnyColor, ColorModel, HsvaColor } from '../types';
import './hue.js';
import './saturation.js';
declare const $h: unique symbol;
declare const $s: unique symbol;
declare const $isSame: unique symbol;
declare const $color: unique symbol;
declare const $hsva: unique symbol;
declare const $change: unique symbol;
export declare const $render: unique symbol;
export declare abstract class ColorPicker<C extends AnyColor> extends HTMLElement {
static get observedAttributes(): string[];
protected abstract get colorModel(): ColorModel<C>;
private _h;
private _s;
private _hsva;
private _color;
private [$h];
private [$s];
private [$hsva];
private [$color];
get color(): C;

@@ -17,6 +24,7 @@ set color(newColor: C);

handleEvent(event: CustomEvent): void;
private _isSame;
protected _render(hsva: HsvaColor): void;
private _change;
private [$isSame];
protected [$render](hsva: HsvaColor): void;
private [$change];
}
export {};
//# sourceMappingURL=color-picker.d.ts.map

@@ -11,2 +11,9 @@ import { equalColorObjects } from '../utils/compare.js';

`);
const $h = Symbol('h');
const $s = Symbol('s');
const $isSame = Symbol('same');
const $color = Symbol('color');
const $hsva = Symbol('hsva');
const $change = Symbol('change');
export const $render = Symbol('render');
export class ColorPicker extends HTMLElement {

@@ -17,4 +24,4 @@ constructor() {

root.addEventListener('move', this);
this._s = root.children[1];
this._h = root.children[2];
this[$s] = root.children[1];
this[$h] = root.children[2];
}

@@ -25,9 +32,9 @@ static get observedAttributes() {

get color() {
return this._color;
return this[$color];
}
set color(newColor) {
if (!this._isSame(newColor)) {
if (!this[$isSame](newColor)) {
const newHsva = this.colorModel.toHsva(newColor);
this._render(newHsva);
this._change(newColor, newHsva);
this[$render](newHsva);
this[$change](newColor, newHsva);
}

@@ -50,3 +57,3 @@ }

const color = this.colorModel.fromAttr(newVal);
if (!this._isSame(color)) {
if (!this[$isSame](color)) {
this.color = color;

@@ -57,20 +64,20 @@ }

// Merge the current HSV color object with updated params.
const newHsva = Object.assign({}, this._hsva, event.detail);
this._render(newHsva);
const newHsva = Object.assign({}, this[$hsva], event.detail);
this[$render](newHsva);
let newColor;
if (!equalColorObjects(newHsva, this._hsva) &&
!this._isSame((newColor = this.colorModel.fromHsva(newHsva)))) {
this._change(newColor, newHsva);
if (!equalColorObjects(newHsva, this[$hsva]) &&
!this[$isSame]((newColor = this.colorModel.fromHsva(newHsva)))) {
this[$change](newColor, newHsva);
}
}
_isSame(color) {
[$isSame](color) {
return this.color && this.colorModel.equal(color, this.color);
}
_render(hsva) {
this._s.hsva = hsva;
this._h.hue = hsva.h;
[$render](hsva) {
this[$s].hsva = hsva;
this[$h].hue = hsva.h;
}
_change(color, hsva) {
this._color = color;
this._hsva = hsva;
[$change](color, hsva) {
this[$color] = color;
this[$hsva] = hsva;
this.dispatchEvent(new CustomEvent('color-changed', { detail: { value: color } }));

@@ -77,0 +84,0 @@ }

@@ -5,5 +5,8 @@ import { Interactive, Interaction } from './interactive.js';

connectedCallback(): void;
private _h;
get xy(): boolean;
get hue(): number;
set hue(h: number);
getMove(interaction: Interaction): Record<string, number>;
getMove(interaction: Interaction, key?: boolean): Record<string, number>;
}
//# sourceMappingURL=hue.d.ts.map
import { Interactive } from './interactive.js';
import { hsvaToHslString } from '../utils/convert.js';
import { createTemplate, createRoot } from '../utils/dom.js';
import { clamp } from '../utils/clamp.js';
import styles from '../styles/hue.js';

@@ -10,2 +11,5 @@ const template = createTemplate(`<style>${styles}</style>`);

createRoot(this, template);
this.setAttribute('aria-label', 'Hue');
this.setAttribute('aria-valuemin', '0');
this.setAttribute('aria-valuemax', '360');
}

@@ -19,3 +23,10 @@ connectedCallback() {

}
get xy() {
return false;
}
get hue() {
return this._h;
}
set hue(h) {
this._h = h;
this.setStyles({

@@ -25,5 +36,7 @@ left: `${(h / 360) * 100}%`,

});
this.setAttribute('aria-valuenow', `${Math.round(h)}`);
}
getMove(interaction) {
return { h: 360 * interaction.left };
getMove(interaction, key) {
// Hue measured in degrees of the color circle ranging from 0 to 360
return { h: key ? clamp(this.hue + interaction.left * 360, 0, 360) : 360 * interaction.left };
}

@@ -30,0 +43,0 @@ }

@@ -12,7 +12,7 @@ export interface Interaction {

set dragging(state: boolean);
handleEvent(event: MouseEvent | TouchEvent): void;
abstract getMove(interaction: Interaction): Record<string, number>;
onMove(event: MouseEvent | TouchEvent): void;
handleEvent(event: Event): void;
abstract getMove(interaction: Interaction, key?: boolean): Record<string, number>;
abstract get xy(): boolean;
setStyles(properties: Record<string, string>): void;
}
//# sourceMappingURL=interactive.d.ts.map
import { createTemplate, createRoot } from '../utils/dom.js';
import { clamp } from '../utils/clamp.js';
import styles from '../styles/interactive.js';
const limit = (number) => (number > 1 ? 1 : number < 0 ? 0 : number);
const template = createTemplate(`
<style>${styles}</style>
<div id="interactive"><div part="pointer"></div></div>
`);
const template = createTemplate(`<style>${styles}</style><div id="interactive"><div part="pointer"></div></div>`);
let hasTouched = false;
// Check if an event was triggered by touch
const isTouch = (e) => window.TouchEvent && e instanceof TouchEvent;
const isTouch = (e) => 'touches' in e;
// Prevent mobile browsers from handling mouse events (conflicting with touch ones).

@@ -20,9 +17,46 @@ // If we detected a touch interaction before, we prefer reacting to touch events only.

};
const getRelativePosition = (rect, event) => {
const pointer = event instanceof MouseEvent ? event : event.touches[0];
return {
left: limit((pointer.pageX - (rect.left + window.pageXOffset)) / rect.width),
top: limit((pointer.pageY - (rect.top + window.pageYOffset)) / rect.height)
};
const fireMove = (target, interaction, key) => {
target.dispatchEvent(new CustomEvent('move', {
bubbles: true,
detail: target.getMove(interaction, key)
}));
};
const pointerMove = (target, event) => {
const pointer = isTouch(event) ? event.touches[0] : event;
const rect = target.getBoundingClientRect();
fireMove(target, {
left: clamp((pointer.pageX - (rect.left + window.pageXOffset)) / rect.width),
top: clamp((pointer.pageY - (rect.top + window.pageYOffset)) / rect.height)
});
};
const keyMove = (target, event) => {
// We use `keyCode` instead of `key` to reduce the size of the library.
const keyCode = event.keyCode;
// Ignore all keys except arrow ones, Page Up, Page Down, Home and End.
if (keyCode > 40 || (target.xy && keyCode < 37) || keyCode < 33)
return;
// Do not scroll page by keys when color picker element has focus.
event.preventDefault();
// Send relative offset to the parent component.
fireMove(target, {
left: keyCode === 39 // Arrow Right
? 0.01
: keyCode === 37 // Arrow Left
? -0.01
: keyCode === 34 // Page Down
? 0.05
: keyCode === 33 // Page Up
? -0.05
: keyCode === 35 // End
? 1
: keyCode === 36 // Home
? -1
: 0,
top: keyCode === 40 // Arrow down
? 0.01
: keyCode === 38 // Arrow Up
? -0.01
: 0
}, true);
};
export class Interactive extends HTMLElement {

@@ -34,2 +68,5 @@ constructor() {

this.addEventListener('touchstart', this);
this.addEventListener('keydown', this);
this.setAttribute('role', 'slider');
this.setAttribute('tabindex', '0');
}

@@ -49,3 +86,3 @@ set dragging(state) {

return;
this.onMove(event);
pointerMove(this, event);
this.dragging = true;

@@ -56,3 +93,3 @@ break;

event.preventDefault();
this.onMove(event);
pointerMove(this, event);
break;

@@ -63,10 +100,7 @@ case 'mouseup':

break;
case 'keydown':
keyMove(this, event);
break;
}
}
onMove(event) {
this.dispatchEvent(new CustomEvent('move', {
bubbles: true,
detail: this.getMove(getRelativePosition(this.getBoundingClientRect(), event))
}));
}
setStyles(properties) {

@@ -73,0 +107,0 @@ for (const p in properties) {

@@ -6,5 +6,8 @@ import { Interactive, Interaction } from './interactive.js';

connectedCallback(): void;
private _hsva;
get xy(): boolean;
get hsva(): HsvaColor;
set hsva(hsva: HsvaColor);
getMove(interaction: Interaction): Record<string, number>;
getMove(interaction: Interaction, key?: boolean): Record<string, number>;
}
//# sourceMappingURL=saturation.d.ts.map
import { Interactive } from './interactive.js';
import { hsvaToHslString } from '../utils/convert.js';
import { createTemplate, createRoot } from '../utils/dom.js';
import { clamp } from '../utils/clamp.js';
import styles from '../styles/saturation.js';

@@ -10,2 +11,3 @@ const template = createTemplate(`<style>${styles}</style>`);

createRoot(this, template);
this.setAttribute('aria-label', 'Color');
}

@@ -19,3 +21,10 @@ connectedCallback() {

}
get xy() {
return true;
}
get hsva() {
return this._hsva;
}
set hsva(hsva) {
this._hsva = hsva;
this.style.backgroundColor = hsvaToHslString({ h: hsva.h, s: 100, v: 100, a: 1 });

@@ -27,5 +36,12 @@ this.setStyles({

});
this.setAttribute('aria-valuetext', `Saturation ${Math.round(hsva.s)}%, Brightness ${Math.round(hsva.v)}%`);
}
getMove(interaction) {
return { s: interaction.left * 100, v: Math.round(100 - interaction.top * 100) };
getMove(interaction, key) {
// Saturation and brightness always fit into [0, 100] range
return {
s: key ? clamp(this.hsva.s + interaction.left * 100, 0, 100) : interaction.left * 100,
v: key
? clamp(this.hsva.v - interaction.top * 100, 0, 100)
: Math.round(100 - interaction.top * 100)
};
}

@@ -32,0 +48,0 @@ }

@@ -1,3 +0,3 @@

declare const _default: "#interactive{position:absolute;left:0;top:0;width:100%;height:100%;touch-action:none;user-select:none;-webkit-user-select:none}[part=pointer]{position:absolute;z-index:1;box-sizing:border-box;width:28px;height:28px;transform:translate(-50%, -50%);background-color:#fff;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2)}[part=pointer]::after{display:block;content:\"\";position:absolute;left:0;top:0;right:0;bottom:0;border-radius:inherit;background-color:currentColor}";
declare const _default: ":host{outline:none}:host(:focus) [part=pointer]{transform:translate(-50%, -50%) scale(1.1)}#interactive{position:absolute;left:0;top:0;width:100%;height:100%;touch-action:none;user-select:none;-webkit-user-select:none}[part=pointer]{position:absolute;z-index:1;box-sizing:border-box;width:28px;height:28px;transform:translate(-50%, -50%);background-color:#fff;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2)}[part=pointer]::after{display:block;content:\"\";position:absolute;left:0;top:0;right:0;bottom:0;border-radius:inherit;background-color:currentColor}";
export default _default;
//# sourceMappingURL=interactive.d.ts.map

@@ -1,2 +0,2 @@

export default `#interactive{position:absolute;left:0;top:0;width:100%;height:100%;touch-action:none;user-select:none;-webkit-user-select:none}[part=pointer]{position:absolute;z-index:1;box-sizing:border-box;width:28px;height:28px;transform:translate(-50%, -50%);background-color:#fff;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2)}[part=pointer]::after{display:block;content:"";position:absolute;left:0;top:0;right:0;bottom:0;border-radius:inherit;background-color:currentColor}`;
export default `:host{outline:none}:host(:focus) [part=pointer]{transform:translate(-50%, -50%) scale(1.1)}#interactive{position:absolute;left:0;top:0;width:100%;height:100%;touch-action:none;user-select:none;-webkit-user-select:none}[part=pointer]{position:absolute;z-index:1;box-sizing:border-box;width:28px;height:28px;transform:translate(-50%, -50%);background-color:#fff;border:2px solid #fff;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,.2)}[part=pointer]::after{display:block;content:"";position:absolute;left:0;top:0;right:0;bottom:0;border-radius:inherit;background-color:currentColor}`;
//# sourceMappingURL=interactive.js.map

@@ -1,3 +0,3 @@

declare const _default: ":host{display:block;position:relative;flex-grow:1;border-bottom:12px solid #000;border-radius:8px 8px 0 0}:host::after,:host::before{content:\"\";position:absolute;left:0;top:0;right:0;bottom:0;pointer-events:none;border-radius:inherit}:host::before{background:linear-gradient(to right, #fff, rgba(255, 255, 255, 0))}:host::after{background:linear-gradient(to top, #000, rgba(0, 0, 0, 0));box-shadow:inset 0 0 0 1px rgba(0,0,0,.05)}[part=pointer]{z-index:3}";
declare const _default: ":host{display:block;position:relative;flex-grow:1;border-bottom:12px solid #000;border-radius:8px 8px 0 0;background-image:linear-gradient(to top, #000, rgba(0, 0, 0, 0)),linear-gradient(to right, #fff, rgba(255, 255, 255, 0));box-shadow:inset 0 0 0 1px rgba(0,0,0,.05)}[part=pointer]{z-index:3}";
export default _default;
//# sourceMappingURL=saturation.d.ts.map

@@ -1,2 +0,2 @@

export default `:host{display:block;position:relative;flex-grow:1;border-bottom:12px solid #000;border-radius:8px 8px 0 0}:host::after,:host::before{content:"";position:absolute;left:0;top:0;right:0;bottom:0;pointer-events:none;border-radius:inherit}:host::before{background:linear-gradient(to right, #fff, rgba(255, 255, 255, 0))}:host::after{background:linear-gradient(to top, #000, rgba(0, 0, 0, 0));box-shadow:inset 0 0 0 1px rgba(0,0,0,.05)}[part=pointer]{z-index:3}`;
export default `:host{display:block;position:relative;flex-grow:1;border-bottom:12px solid #000;border-radius:8px 8px 0 0;background-image:linear-gradient(to top, #000, rgba(0, 0, 0, 0)),linear-gradient(to right, #fff, rgba(255, 255, 255, 0));box-shadow:inset 0 0 0 1px rgba(0,0,0,.05)}[part=pointer]{z-index:3}`;
//# sourceMappingURL=saturation.js.map
{
"name": "vanilla-colorful",
"version": "0.3.1",
"version": "0.4.0",
"description": "A tiny framework agnostic color picker element for modern web apps",

@@ -17,2 +17,19 @@ "author": "Serhii Kulykov <iamkulykov@gmail.com>",

"homepage": "https://web-padawan.github.io/vanilla-colorful/",
"keywords": [
"webcomponents",
"web-components",
"webcomponent",
"web-component",
"custom-element",
"customelement",
"colorpicker",
"hex",
"color",
"color-picker",
"accessible",
"accessibility",
"aria",
"a11y",
"wai-aria"
],
"files": [

@@ -62,51 +79,51 @@ "ACKNOWLEDGMENTS",

"path": "hex-color-picker.js",
"limit": "2.3 KB"
"limit": "2.6 KB"
},
{
"path": "hsl-color-picker.js",
"limit": "2.1 KB"
"limit": "2.4 KB"
},
{
"path": "hsl-string-color-picker.js",
"limit": "2.2 KB"
"limit": "2.5 KB"
},
{
"path": "hsla-color-picker.js",
"limit": "2.4 KB"
"limit": "2.7 KB"
},
{
"path": "hsla-string-color-picker.js",
"limit": "2.5 KB"
"limit": "2.8 KB"
},
{
"path": "hsv-color-picker.js",
"limit": "2 KB"
"limit": "2.3 KB"
},
{
"path": "hsv-string-color-picker.js",
"limit": "2.1 KB"
"limit": "2.4 KB"
},
{
"path": "hsva-color-picker.js",
"limit": "2.3 KB"
"limit": "2.7 KB"
},
{
"path": "hsva-string-color-picker.js",
"limit": "2.5 KB"
"limit": "2.8 KB"
},
{
"path": "rgb-color-picker.js",
"limit": "2.2 KB"
"limit": "2.5 KB"
},
{
"path": "rgb-string-color-picker.js",
"limit": "2.3 KB"
"limit": "2.6 KB"
},
{
"path": "rgba-color-picker.js",
"limit": "2.5 KB"
"limit": "2.9 KB"
},
{
"path": "rgba-string-color-picker.js",
"limit": "2.6 KB"
"limit": "3 KB"
},

@@ -113,0 +130,0 @@ {

@@ -25,6 +25,7 @@ <div align="center">

- **Small**: Just 2,3 KB (minified and gzipped). [Size Limit](https://github.com/ai/size-limit) controls the size.
- **Small**: Just 2,7 KB (minified and gzipped). [Size Limit](https://github.com/ai/size-limit) controls the size.
- **Fast**: Built with standards based Custom Elements.
- **Bulletproof**: Written in strict TypeScript and covered by 60+ tests.
- **Bulletproof**: Written in strict TypeScript and covered by 100+ tests.
- **Simple**: The interface is straight forward and easy to use.
- **Accessible**: Follows the [WAI-ARIA](https://www.w3.org/WAI/standards-guidelines/aria/) guidelines to support users of assistive technologies.
- **Mobile-friendly**: Works well on mobile devices and touch screens.

@@ -72,7 +73,25 @@ - **Framework-agnostic**: Can be used [with any framework](https://custom-elements-everywhere.com/).

picker.addEventListener('color-changed', (event) => {
// get updated color value
const newColor = event.detail.value;
});
// get current color value
console.log(picker.color);
</script>
```
## ES modules
**vanilla-colorful** is authored using ES modules which are [natively supported](https://caniuse.com/es6-module)
by modern browsers. However, it also uses "bare module imports" which are [not yet standardized](https://github.com/WICG/import-maps)
and require a small transform.
We recommend the following tools for the ES modules based development:
- [`@web/dev-server`](https://modern-web.dev/docs/dev-server/overview/) resolves bare module imports on the fly.
- [`snowpack`](https://www.snowpack.dev) performs one-time transform when installing dependencies.
- [`@rollup/plugin-node-resolve`](https://github.com/rollup/plugins/tree/master/packages/node-resolve) is needed when using Rollup.
None of these tools are needed when importing the component from CDN.
## Supported color models

@@ -79,0 +98,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc