You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

@openpanel/sdk

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@openpanel/sdk - npm Package Compare versions

Comparing version
1.0.3
to
1.0.4
+129
README.md
# Javascript (Node / Generic)
The OpenPanel Web SDK allows you to track user behavior on your website using a simple script tag. This guide provides instructions for installing and using the Web SDK in your project.
> 📖 **Full documentation:** [https://openpanel.dev/docs/sdks/sdk](https://openpanel.dev/docs/sdks/sdk)
---
## Installation
### Step 1: Install
```npm
npm install @openpanel/sdk
```
### Step 2: Initialize
```js title="op.ts"
import { OpenPanel } from '@openpanel/sdk';
const op = new OpenPanel({
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET',
});
```
#### Options
##### Common options
- `apiUrl` - The url of the openpanel API or your self-hosted instance
- `clientId` - The client id of your application
- `clientSecret` - The client secret of your application (**only required for server-side events**)
- `filter` - A function that will be called before sending an event. If it returns false, the event will not be sent
- `disabled` - If true, the library will not send any events
### Step 3: Usage
```js title="main.ts"
import { op } from './op.js';
op.track('my_event', { foo: 'bar' });
```
## Usage
### Tracking Events
You can track events with two different methods: by calling the `op.track( directly or by adding `data-track` attributes to your HTML elements.
```ts title="index.ts"
import { op } from './op.ts';
op.track('my_event', { foo: 'bar' });
```
### Identifying Users
To identify a user, call the `op.identify( method with a unique identifier.
```js title="index.js"
import { op } from './op.ts';
op.identify({
profileId: '123', // Required
firstName: 'Joe',
lastName: 'Doe',
email: 'joe@doe.com',
properties: {
tier: 'premium',
},
});
```
### Setting Global Properties
To set properties that will be sent with every event:
```js title="index.js"
import { op } from './op.ts'
op.setGlobalProperties({
app_version: '1.0.2',
environment: 'production',
});
```
### Incrementing Properties
To increment a numeric property on a user profile.
- `value` is the amount to increment the property by. If not provided, the property will be incremented by 1.
```js title="index.js"
import { op } from './op.ts'
op.increment({
profileId: '1',
property: 'visits',
value: 1 // optional
});
```
### Decrementing Properties
To decrement a numeric property on a user profile.
- `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1.
```js title="index.js"
import { op } from './op.ts'
op.decrement({
profileId: '1',
property: 'visits',
value: 1 // optional
});
```
### Clearing User Data
To clear the current user's data:
```js title="index.js"
import { op } from './op.ts'
op.clear()
```
+1
-1

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

"use strict";var l=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var u=Object.prototype.hasOwnProperty;var f=(n,e)=>{for(var r in e)l(n,r,{get:e[r],enumerable:!0})},g=(n,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of y(e))!u.call(n,t)&&t!==r&&l(n,t,{get:()=>e[t],enumerable:!(i=c(e,t))||i.enumerable});return n};var h=n=>g(l({},"__esModule",{value:!0}),n);var m={};f(m,{OpenPanel:()=>d});module.exports=h(m);var o=class{constructor(e){this.baseUrl=e.baseUrl,this.headers={"Content-Type":"application/json",...e.defaultHeaders},this.maxRetries=e.maxRetries??3,this.initialRetryDelay=e.initialRetryDelay??500}async resolveHeaders(){let e={};for(let[r,i]of Object.entries(this.headers)){let t=await i;t!==null&&(e[r]=t)}return e}addHeader(e,r){this.headers[e]=r}async post(e,r,i,t){try{let s=await fetch(e,{method:"POST",headers:await this.resolveHeaders(),body:r?JSON.stringify(r??{}):void 0,keepalive:!0,...i});if(s.status===401)return null;if(s.status!==200&&s.status!==202)throw new Error(`HTTP error! status: ${s.status}`);let a=await s.text();return a?JSON.parse(a):null}catch(s){if(t<this.maxRetries){let a=this.initialRetryDelay*2**t;return await new Promise(p=>setTimeout(p,a)),this.post(e,r,i,t+1)}return console.error("Max retries reached:",s),null}}async fetch(e,r,i={}){let t=`${this.baseUrl}${e}`;return this.post(t,r,i,0)}};var d=class{constructor(e){this.options=e;this.queue=[];let r={"openpanel-client-id":e.clientId};e.clientSecret&&(r["openpanel-client-secret"]=e.clientSecret),r["openpanel-sdk-name"]=e.sdk||"node",r["openpanel-sdk-version"]=e.sdkVersion||"1.0.3",this.api=new o({baseUrl:e.apiUrl||"https://api.openpanel.dev",defaultHeaders:r})}init(){}ready(){this.options.waitForProfile=!1,this.flush()}async send(e){return this.options.disabled||this.options.filter&&!this.options.filter(e)?Promise.resolve():this.options.waitForProfile&&!this.profileId?(this.queue.push(e),Promise.resolve()):this.api.fetch("/track",e)}setGlobalProperties(e){this.global={...this.global,...e}}async track(e,r){return this.log("track event",e,r),this.send({type:"track",payload:{name:e,profileId:r?.profileId??this.profileId,properties:{...this.global??{},...r??{}}}})}async identify(e){if(this.log("identify user",e),e.profileId&&(this.profileId=e.profileId,this.flush()),Object.keys(e).length>1)return this.send({type:"identify",payload:{...e,properties:{...this.global,...e.properties}}})}async alias(e){}async increment(e){return this.send({type:"increment",payload:e})}async decrement(e){return this.send({type:"decrement",payload:e})}async revenue(e,r){let i=r?.deviceId;return delete r?.deviceId,this.track("revenue",{...r??{},...i?{__deviceId:i}:{},__revenue:e})}async fetchDeviceId(){return(await this.api.fetch("/track/device-id",void 0,{method:"GET",keepalive:!1}))?.deviceId??""}clear(){this.profileId=void 0}flush(){this.queue.forEach(e=>{this.send({...e,payload:{...e.payload,profileId:e.payload.profileId??this.profileId}})}),this.queue=[]}log(...e){this.options.debug&&console.log("[OpenPanel.dev]",...e)}};0&&(module.exports={OpenPanel});
"use strict";var l=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var u=Object.prototype.hasOwnProperty;var h=(s,e)=>{for(var r in e)l(s,r,{get:e[r],enumerable:!0})},f=(s,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of p(e))!u.call(s,t)&&t!==r&&l(s,t,{get:()=>e[t],enumerable:!(i=y(e,t))||i.enumerable});return s};var P=s=>f(l({},"__esModule",{value:!0}),s);var g={};h(g,{OpenPanel:()=>d});module.exports=P(g);var o=class{constructor(e){this.baseUrl=e.baseUrl,this.headers={"Content-Type":"application/json",...e.defaultHeaders},this.maxRetries=e.maxRetries??3,this.initialRetryDelay=e.initialRetryDelay??500}async resolveHeaders(){let e={};for(let[r,i]of Object.entries(this.headers)){let t=await i;t!==null&&(e[r]=t)}return e}addHeader(e,r){this.headers[e]=r}async post(e,r,i,t){try{let n=await fetch(e,{method:"POST",headers:await this.resolveHeaders(),body:r?JSON.stringify(r??{}):void 0,keepalive:!0,...i});if(n.status===401)return null;if(n.status!==200&&n.status!==202)throw new Error(`HTTP error! status: ${n.status}`);let a=await n.text();return a?JSON.parse(a):null}catch(n){if(t<this.maxRetries){let a=this.initialRetryDelay*2**t;return await new Promise(c=>setTimeout(c,a)),this.post(e,r,i,t+1)}return console.error("Max retries reached:",n),null}}async fetch(e,r,i={}){let t=`${this.baseUrl}${e}`;return this.post(t,r,i,0)}};var d=class{constructor(e){this.options=e;this.queue=[];let r={"openpanel-client-id":e.clientId};e.clientSecret&&(r["openpanel-client-secret"]=e.clientSecret),r["openpanel-sdk-name"]=e.sdk||"node",r["openpanel-sdk-version"]=e.sdkVersion||"1.0.4",this.api=new o({baseUrl:e.apiUrl||"https://api.openpanel.dev",defaultHeaders:r})}init(){}ready(){this.options.waitForProfile=!1,this.flush()}async send(e){return this.options.disabled||this.options.filter&&!this.options.filter(e)?Promise.resolve():this.options.waitForProfile&&!this.profileId?(this.queue.push(e),Promise.resolve()):this.api.fetch("/track",e)}setGlobalProperties(e){this.global={...this.global,...e}}async track(e,r){return this.log("track event",e,r),this.send({type:"track",payload:{name:e,profileId:r?.profileId??this.profileId,properties:{...this.global??{},...r??{}}}})}async identify(e){if(this.log("identify user",e),e.profileId&&(this.profileId=e.profileId,this.flush()),Object.keys(e).length>1)return this.send({type:"identify",payload:{...e,properties:{...this.global,...e.properties}}})}async alias(e){}async increment(e){return this.send({type:"increment",payload:e})}async decrement(e){return this.send({type:"decrement",payload:e})}async revenue(e,r){let i=r?.deviceId;return delete r?.deviceId,this.track("revenue",{...r??{},...i?{__deviceId:i}:{},__revenue:e})}async fetchDeviceId(){return(await this.api.fetch("/track/device-id",void 0,{method:"GET",keepalive:!1}))?.deviceId??""}clear(){this.profileId=void 0}flush(){this.queue.forEach(e=>{this.send({...e,payload:{...e.payload,profileId:e.payload.profileId??this.profileId}})}),this.queue=[]}log(...e){this.options.debug&&console.log("[OpenPanel.dev]",...e)}};0&&(module.exports={OpenPanel});

@@ -0,1 +1,4 @@

import { ITrackHandlerPayload, IIdentifyPayload, IAliasPayload, IIncrementPayload, IDecrementPayload } from '@openpanel/validation';
export { IAliasPayload as AliasPayload, IDecrementPayload as DecrementPayload, IIdentifyPayload as IdentifyPayload, IIncrementPayload as IncrementPayload, ITrackHandlerPayload as TrackHandlerPayload, ITrackPayload as TrackPayload } from '@openpanel/validation';
interface ApiConfig {

@@ -22,23 +25,2 @@ baseUrl: string;

type TrackHandlerPayload = {
type: 'track';
payload: TrackPayload;
} | {
type: 'increment';
payload: IncrementPayload;
} | {
type: 'decrement';
payload: DecrementPayload;
} | {
type: 'alias';
payload: AliasPayload;
} | {
type: 'identify';
payload: IdentifyPayload;
};
type TrackPayload = {
name: string;
properties?: Record<string, unknown>;
profileId?: string;
};
type TrackProperties = {

@@ -48,24 +30,2 @@ [key: string]: unknown;

};
type IdentifyPayload = {
profileId: string;
firstName?: string;
lastName?: string;
email?: string;
avatar?: string;
properties?: Record<string, unknown>;
};
type AliasPayload = {
profileId: string;
alias: string;
};
type IncrementPayload = {
profileId: string;
property: string;
value?: number;
};
type DecrementPayload = {
profileId: string;
property: string;
value?: number;
};
type OpenPanelOptions = {

@@ -78,3 +38,3 @@ clientId: string;

waitForProfile?: boolean;
filter?: (payload: TrackHandlerPayload) => boolean;
filter?: (payload: ITrackHandlerPayload) => boolean;
disabled?: boolean;

@@ -88,16 +48,16 @@ debug?: boolean;

global?: Record<string, unknown>;
queue: TrackHandlerPayload[];
queue: ITrackHandlerPayload[];
constructor(options: OpenPanelOptions);
init(): void;
ready(): void;
send(payload: TrackHandlerPayload): Promise<unknown>;
send(payload: ITrackHandlerPayload): Promise<unknown>;
setGlobalProperties(properties: Record<string, unknown>): void;
track(name: string, properties?: TrackProperties): Promise<unknown>;
identify(payload: IdentifyPayload): Promise<unknown>;
identify(payload: IIdentifyPayload): Promise<unknown>;
/**
* @deprecated This method is deprecated and will be removed in a future version.
*/
alias(payload: AliasPayload): Promise<void>;
increment(payload: IncrementPayload): Promise<unknown>;
decrement(payload: DecrementPayload): Promise<unknown>;
alias(payload: IAliasPayload): Promise<void>;
increment(payload: IIncrementPayload): Promise<unknown>;
decrement(payload: IDecrementPayload): Promise<unknown>;
revenue(amount: number, properties?: TrackProperties & {

@@ -112,30 +72,2 @@ deviceId?: string;

interface OpenpanelEventOptions {
profileId?: string;
}
interface PostEventPayload {
name: string;
timestamp: string;
profileId?: string;
properties?: Record<string, unknown> & OpenpanelEventOptions;
}
interface UpdateProfilePayload {
profileId: string;
firstName?: string;
lastName?: string;
email?: string;
avatar?: string;
properties?: Record<string, unknown>;
}
interface IncrementProfilePayload {
profileId: string;
property: string;
value: number;
}
interface DecrementProfilePayload {
profileId?: string;
property: string;
value: number;
}
export { type AliasPayload, type DecrementPayload, type DecrementProfilePayload, type IdentifyPayload, type IncrementPayload, type IncrementProfilePayload, OpenPanel, type OpenPanelOptions, type OpenpanelEventOptions, type PostEventPayload, type TrackHandlerPayload, type TrackPayload, type TrackProperties, type UpdateProfilePayload };
export { OpenPanel, type OpenPanelOptions, type TrackProperties };

@@ -0,1 +1,4 @@

import { ITrackHandlerPayload, IIdentifyPayload, IAliasPayload, IIncrementPayload, IDecrementPayload } from '@openpanel/validation';
export { IAliasPayload as AliasPayload, IDecrementPayload as DecrementPayload, IIdentifyPayload as IdentifyPayload, IIncrementPayload as IncrementPayload, ITrackHandlerPayload as TrackHandlerPayload, ITrackPayload as TrackPayload } from '@openpanel/validation';
interface ApiConfig {

@@ -22,23 +25,2 @@ baseUrl: string;

type TrackHandlerPayload = {
type: 'track';
payload: TrackPayload;
} | {
type: 'increment';
payload: IncrementPayload;
} | {
type: 'decrement';
payload: DecrementPayload;
} | {
type: 'alias';
payload: AliasPayload;
} | {
type: 'identify';
payload: IdentifyPayload;
};
type TrackPayload = {
name: string;
properties?: Record<string, unknown>;
profileId?: string;
};
type TrackProperties = {

@@ -48,24 +30,2 @@ [key: string]: unknown;

};
type IdentifyPayload = {
profileId: string;
firstName?: string;
lastName?: string;
email?: string;
avatar?: string;
properties?: Record<string, unknown>;
};
type AliasPayload = {
profileId: string;
alias: string;
};
type IncrementPayload = {
profileId: string;
property: string;
value?: number;
};
type DecrementPayload = {
profileId: string;
property: string;
value?: number;
};
type OpenPanelOptions = {

@@ -78,3 +38,3 @@ clientId: string;

waitForProfile?: boolean;
filter?: (payload: TrackHandlerPayload) => boolean;
filter?: (payload: ITrackHandlerPayload) => boolean;
disabled?: boolean;

@@ -88,16 +48,16 @@ debug?: boolean;

global?: Record<string, unknown>;
queue: TrackHandlerPayload[];
queue: ITrackHandlerPayload[];
constructor(options: OpenPanelOptions);
init(): void;
ready(): void;
send(payload: TrackHandlerPayload): Promise<unknown>;
send(payload: ITrackHandlerPayload): Promise<unknown>;
setGlobalProperties(properties: Record<string, unknown>): void;
track(name: string, properties?: TrackProperties): Promise<unknown>;
identify(payload: IdentifyPayload): Promise<unknown>;
identify(payload: IIdentifyPayload): Promise<unknown>;
/**
* @deprecated This method is deprecated and will be removed in a future version.
*/
alias(payload: AliasPayload): Promise<void>;
increment(payload: IncrementPayload): Promise<unknown>;
decrement(payload: DecrementPayload): Promise<unknown>;
alias(payload: IAliasPayload): Promise<void>;
increment(payload: IIncrementPayload): Promise<unknown>;
decrement(payload: IDecrementPayload): Promise<unknown>;
revenue(amount: number, properties?: TrackProperties & {

@@ -112,30 +72,2 @@ deviceId?: string;

interface OpenpanelEventOptions {
profileId?: string;
}
interface PostEventPayload {
name: string;
timestamp: string;
profileId?: string;
properties?: Record<string, unknown> & OpenpanelEventOptions;
}
interface UpdateProfilePayload {
profileId: string;
firstName?: string;
lastName?: string;
email?: string;
avatar?: string;
properties?: Record<string, unknown>;
}
interface IncrementProfilePayload {
profileId: string;
property: string;
value: number;
}
interface DecrementProfilePayload {
profileId?: string;
property: string;
value: number;
}
export { type AliasPayload, type DecrementPayload, type DecrementProfilePayload, type IdentifyPayload, type IncrementPayload, type IncrementProfilePayload, OpenPanel, type OpenPanelOptions, type OpenpanelEventOptions, type PostEventPayload, type TrackHandlerPayload, type TrackPayload, type TrackProperties, type UpdateProfilePayload };
export { OpenPanel, type OpenPanelOptions, type TrackProperties };

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

var a=class{constructor(e){this.baseUrl=e.baseUrl,this.headers={"Content-Type":"application/json",...e.defaultHeaders},this.maxRetries=e.maxRetries??3,this.initialRetryDelay=e.initialRetryDelay??500}async resolveHeaders(){let e={};for(let[r,t]of Object.entries(this.headers)){let i=await t;i!==null&&(e[r]=i)}return e}addHeader(e,r){this.headers[e]=r}async post(e,r,t,i){try{let n=await fetch(e,{method:"POST",headers:await this.resolveHeaders(),body:r?JSON.stringify(r??{}):void 0,keepalive:!0,...t});if(n.status===401)return null;if(n.status!==200&&n.status!==202)throw new Error(`HTTP error! status: ${n.status}`);let s=await n.text();return s?JSON.parse(s):null}catch(n){if(i<this.maxRetries){let s=this.initialRetryDelay*2**i;return await new Promise(d=>setTimeout(d,s)),this.post(e,r,t,i+1)}return console.error("Max retries reached:",n),null}}async fetch(e,r,t={}){let i=`${this.baseUrl}${e}`;return this.post(i,r,t,0)}};var o=class{constructor(e){this.options=e;this.queue=[];let r={"openpanel-client-id":e.clientId};e.clientSecret&&(r["openpanel-client-secret"]=e.clientSecret),r["openpanel-sdk-name"]=e.sdk||"node",r["openpanel-sdk-version"]=e.sdkVersion||"1.0.3",this.api=new a({baseUrl:e.apiUrl||"https://api.openpanel.dev",defaultHeaders:r})}init(){}ready(){this.options.waitForProfile=!1,this.flush()}async send(e){return this.options.disabled||this.options.filter&&!this.options.filter(e)?Promise.resolve():this.options.waitForProfile&&!this.profileId?(this.queue.push(e),Promise.resolve()):this.api.fetch("/track",e)}setGlobalProperties(e){this.global={...this.global,...e}}async track(e,r){return this.log("track event",e,r),this.send({type:"track",payload:{name:e,profileId:r?.profileId??this.profileId,properties:{...this.global??{},...r??{}}}})}async identify(e){if(this.log("identify user",e),e.profileId&&(this.profileId=e.profileId,this.flush()),Object.keys(e).length>1)return this.send({type:"identify",payload:{...e,properties:{...this.global,...e.properties}}})}async alias(e){}async increment(e){return this.send({type:"increment",payload:e})}async decrement(e){return this.send({type:"decrement",payload:e})}async revenue(e,r){let t=r?.deviceId;return delete r?.deviceId,this.track("revenue",{...r??{},...t?{__deviceId:t}:{},__revenue:e})}async fetchDeviceId(){return(await this.api.fetch("/track/device-id",void 0,{method:"GET",keepalive:!1}))?.deviceId??""}clear(){this.profileId=void 0}flush(){this.queue.forEach(e=>{this.send({...e,payload:{...e.payload,profileId:e.payload.profileId??this.profileId}})}),this.queue=[]}log(...e){this.options.debug&&console.log("[OpenPanel.dev]",...e)}};export{o as OpenPanel};
var a=class{constructor(e){this.baseUrl=e.baseUrl,this.headers={"Content-Type":"application/json",...e.defaultHeaders},this.maxRetries=e.maxRetries??3,this.initialRetryDelay=e.initialRetryDelay??500}async resolveHeaders(){let e={};for(let[r,t]of Object.entries(this.headers)){let i=await t;i!==null&&(e[r]=i)}return e}addHeader(e,r){this.headers[e]=r}async post(e,r,t,i){try{let s=await fetch(e,{method:"POST",headers:await this.resolveHeaders(),body:r?JSON.stringify(r??{}):void 0,keepalive:!0,...t});if(s.status===401)return null;if(s.status!==200&&s.status!==202)throw new Error(`HTTP error! status: ${s.status}`);let n=await s.text();return n?JSON.parse(n):null}catch(s){if(i<this.maxRetries){let n=this.initialRetryDelay*2**i;return await new Promise(d=>setTimeout(d,n)),this.post(e,r,t,i+1)}return console.error("Max retries reached:",s),null}}async fetch(e,r,t={}){let i=`${this.baseUrl}${e}`;return this.post(i,r,t,0)}};var o=class{constructor(e){this.options=e;this.queue=[];let r={"openpanel-client-id":e.clientId};e.clientSecret&&(r["openpanel-client-secret"]=e.clientSecret),r["openpanel-sdk-name"]=e.sdk||"node",r["openpanel-sdk-version"]=e.sdkVersion||"1.0.4",this.api=new a({baseUrl:e.apiUrl||"https://api.openpanel.dev",defaultHeaders:r})}init(){}ready(){this.options.waitForProfile=!1,this.flush()}async send(e){return this.options.disabled||this.options.filter&&!this.options.filter(e)?Promise.resolve():this.options.waitForProfile&&!this.profileId?(this.queue.push(e),Promise.resolve()):this.api.fetch("/track",e)}setGlobalProperties(e){this.global={...this.global,...e}}async track(e,r){return this.log("track event",e,r),this.send({type:"track",payload:{name:e,profileId:r?.profileId??this.profileId,properties:{...this.global??{},...r??{}}}})}async identify(e){if(this.log("identify user",e),e.profileId&&(this.profileId=e.profileId,this.flush()),Object.keys(e).length>1)return this.send({type:"identify",payload:{...e,properties:{...this.global,...e.properties}}})}async alias(e){}async increment(e){return this.send({type:"increment",payload:e})}async decrement(e){return this.send({type:"decrement",payload:e})}async revenue(e,r){let t=r?.deviceId;return delete r?.deviceId,this.track("revenue",{...r??{},...t?{__deviceId:t}:{},__revenue:e})}async fetchDeviceId(){return(await this.api.fetch("/track/device-id",void 0,{method:"GET",keepalive:!1}))?.deviceId??""}clear(){this.profileId=void 0}flush(){this.queue.forEach(e=>{this.send({...e,payload:{...e.payload,profileId:e.payload.profileId??this.profileId}})}),this.queue=[]}log(...e){this.options.debug&&console.log("[OpenPanel.dev]",...e)}};export{o as OpenPanel};
{
"name": "@openpanel/sdk",
"version": "1.0.3",
"version": "1.0.4",
"module": "./dist/index.js",
"config": {
"docPath": "apps/public/content/docs/(tracking)/sdks/javascript.mdx"
},
"scripts": {

@@ -12,2 +15,3 @@ "build": "rm -rf dist && tsup",

"@openpanel/tsconfig": "workspace:*",
"@openpanel/validation": "workspace:*",
"@types/node": "catalog:",

@@ -21,3 +25,3 @@ "tsup": "^7.2.0",

"types": "./dist/index.d.ts",
"files": ["dist"],
"files": ["dist", "README.md"],
"exports": {

@@ -24,0 +28,0 @@ ".": {