@chayns-components/typewriter 5.0.0-beta.997 → 5.0.0

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.
@@ -1,51 +1,113 @@
1
1
  /**
2
- * This function extracts a part of the text from an HTML text. The HTML elements themselves are
3
- * returned in the result. In addition, the function ensures that the closing tag of the Bold HTML
4
- * element is also returned for text that is cut off in the middle of a Bold element, for example.
2
+ * Returns a substring of an HTML string while preserving HTML structure.
5
3
  *
6
- * @param html - The text from which a part should be taken
7
- * @param length - The length of the text to be extracted
4
+ * Core rules:
5
+ * - Element nodes are re-serialized as tags (start/end) to keep structure.
6
+ * - Text nodes are always HTML-escaped on output. This prevents that previously
7
+ * escaped text (like "<div>") turns into real tags during the DOM round trip.
8
+ * - Attribute values are HTML-escaped on output.
9
+ * - Void elements are serialized without closing tags.
10
+ * - For TWIGNORE/TW-IGNORE elements, the innerHTML is passed through so that
11
+ * their content (including real HTML) remains untouched.
12
+ * - On early cutoff (once the length limit is reached), already opened tags are
13
+ * properly closed to keep the result valid HTML.
8
14
  *
9
- * @return string - The text part with the specified length - additionally the HTML elements are added
15
+ * Note on length counting:
16
+ * - The length is based on the decoded textContent length (as the DOM provides),
17
+ * not on byte length nor escaped entity length. This mirrors how the text is perceived.
18
+ *
19
+ * @param html The input HTML string; may contain a mix of real HTML and already escaped HTML.
20
+ * @param length The maximum number of text characters (based on textContent) to include.
21
+ * @returns A valid HTML string containing up to the specified number of text characters,
22
+ * preserving HTML tags and keeping escaped text escaped.
10
23
  */
11
24
  export const getSubTextFromHTML = (html, length) => {
12
25
  const div = document.createElement('div');
13
26
  div.innerHTML = html;
14
27
  let text = '';
15
28
  let currLength = 0;
16
- const traverse = element => {
17
- if (element.nodeName === 'TWIGNORE') {
18
- text += element.innerHTML;
19
- } else if (element.nodeType === 3 && typeof element.textContent === 'string') {
20
- const nodeText = element.textContent;
21
- if (currLength + nodeText.length <= length) {
22
- text += nodeText;
29
+
30
+ // Escape text node content to ensure that decoded "<" and ">" do not become real tags.
31
+ const escapeText = value => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
32
+
33
+ // Escape attribute values safely.
34
+ const escapeAttr = value => String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
35
+
36
+ // HTML void elements (must not have closing tags)
37
+ const VOID_ELEMENTS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
38
+
39
+ // Traverses nodes and appends to "text".
40
+ // Returns false to signal "stop traversal" once the length limit is reached.
41
+ const traverse = node => {
42
+ // Text node
43
+ if (node.nodeType === 3 && typeof node.textContent === 'string') {
44
+ const nodeText = node.textContent;
45
+ const remaining = length - currLength;
46
+ if (remaining <= 0) {
47
+ return false;
48
+ }
49
+ if (nodeText.length <= remaining) {
50
+ // Always escape text before writing to output
51
+ text += escapeText(nodeText);
23
52
  currLength += nodeText.length;
24
53
  } else {
25
- text += nodeText.substring(0, length - currLength);
54
+ // Cut the text and stop traversal
55
+ text += escapeText(nodeText.substring(0, remaining));
56
+ currLength += remaining;
26
57
  return false;
27
58
  }
28
- } else if (element.nodeType === 1) {
59
+ return true;
60
+ }
61
+
62
+ // Element node
63
+ if (node.nodeType === 1) {
64
+ const element = node;
65
+
66
+ // Pass-through for TWIGNORE/TW-IGNORE: keep their HTML as-is.
67
+ if (element.nodeName === 'TWIGNORE' || element.nodeName === 'TW-IGNORE') {
68
+ // element.innerHTML serializes children; escaped text stays escaped,
69
+ // real HTML stays HTML — exactly what we want here.
70
+ text += element.innerHTML;
71
+ return true;
72
+ }
29
73
  const nodeName = element.nodeName.toLowerCase();
30
- let attributes = '';
31
74
 
32
- // @ts-expect-error: Type is correct here
75
+ // Serialize attributes safely
76
+ let attributes = '';
77
+ // @ts-expect-error: attributes is a NodeListOf<Attr>
33
78
  // eslint-disable-next-line no-restricted-syntax
34
79
  for (const attribute of element.attributes) {
35
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions
36
- attributes += ` ${attribute.name}="${attribute.value}"`;
80
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions,@typescript-eslint/no-unsafe-argument
81
+ attributes += ` ${attribute.name}="${escapeAttr(attribute.value)}"`;
37
82
  }
83
+
84
+ // Open tag
38
85
  text += `<${nodeName}${attributes}>`;
39
- for (let i = 0; i < element.childNodes.length; i++) {
40
- const childNode = element.childNodes[i];
41
- if (childNode && !traverse(childNode)) {
42
- return false;
86
+
87
+ // Void elements: do not recurse children and do not emit a closing tag
88
+ const isVoid = VOID_ELEMENTS.has(nodeName);
89
+ if (!isVoid) {
90
+ // Recurse through children until limit is reached
91
+ for (let i = 0; i < element.childNodes.length; i++) {
92
+ const childNode = element.childNodes[i];
93
+ if (childNode && !traverse(childNode)) {
94
+ // On early stop: close this tag to keep valid HTML, then bubble stop.
95
+ text += `</${nodeName}>`;
96
+ return false;
97
+ }
43
98
  }
99
+
100
+ // Close tag after all children
101
+ text += `</${nodeName}>`;
44
102
  }
45
- text += `</${nodeName}>`;
103
+ return true;
46
104
  }
105
+
106
+ // Other node types (comments, etc.) are ignored for text length
47
107
  return true;
48
108
  };
109
+
110
+ // Traverse top-level children
49
111
  for (let i = 0; i < div.childNodes.length; i++) {
50
112
  const childNode = div.childNodes[i];
51
113
  if (childNode && !traverse(childNode)) {
@@ -78,16 +140,17 @@ export const shuffleArray = array => {
78
140
  const result = Array.from(array);
79
141
  for (let i = result.length - 1; i > 0; i--) {
80
142
  const j = Math.floor(Math.random() * (i + 1));
143
+
144
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
81
145
  [result[i], result[j]] = [result[j], result[i]];
82
146
  }
83
147
  return result;
84
148
  };
85
- export const calculateAutoSpeed = _ref => {
86
- let {
87
- fullTextLength,
88
- currentPosition,
89
- baseSpeedFactor
90
- } = _ref;
149
+ export const calculateAutoSpeed = ({
150
+ fullTextLength,
151
+ currentPosition,
152
+ baseSpeedFactor
153
+ }) => {
91
154
  const MIN_SPEED = 1;
92
155
  const MAX_SPEED = 10;
93
156
  const remainingLength = fullTextLength - currentPosition;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":["getSubTextFromHTML","html","length","div","document","createElement","innerHTML","text","currLength","traverse","element","nodeName","nodeType","textContent","nodeText","substring","toLowerCase","attributes","attribute","name","value","i","childNodes","childNode","getCharactersCount","count","node","trim","Array","from","forEach","shuffleArray","array","result","j","Math","floor","random","calculateAutoSpeed","_ref","fullTextLength","currentPosition","baseSpeedFactor","MIN_SPEED","MAX_SPEED","remainingLength","speed","min","steps"],"sources":["../../../../src/components/typewriter/utils.ts"],"sourcesContent":["/**\n * This function extracts a part of the text from an HTML text. The HTML elements themselves are\n * returned in the result. In addition, the function ensures that the closing tag of the Bold HTML\n * element is also returned for text that is cut off in the middle of a Bold element, for example.\n *\n * @param html - The text from which a part should be taken\n * @param length - The length of the text to be extracted\n *\n * @return string - The text part with the specified length - additionally the HTML elements are added\n */\nexport const getSubTextFromHTML = (html: string, length: number): string => {\n const div = document.createElement('div');\n\n div.innerHTML = html;\n\n let text = '';\n let currLength = 0;\n\n const traverse = (element: Element): boolean => {\n if (element.nodeName === 'TWIGNORE') {\n text += element.innerHTML;\n } else if (element.nodeType === 3 && typeof element.textContent === 'string') {\n const nodeText = element.textContent;\n\n if (currLength + nodeText.length <= length) {\n text += nodeText;\n currLength += nodeText.length;\n } else {\n text += nodeText.substring(0, length - currLength);\n\n return false;\n }\n } else if (element.nodeType === 1) {\n const nodeName = element.nodeName.toLowerCase();\n\n let attributes = '';\n\n // @ts-expect-error: Type is correct here\n // eslint-disable-next-line no-restricted-syntax\n for (const attribute of element.attributes) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions\n attributes += ` ${attribute.name}=\"${attribute.value}\"`;\n }\n\n text += `<${nodeName}${attributes}>`;\n\n for (let i = 0; i < element.childNodes.length; i++) {\n const childNode = element.childNodes[i];\n\n if (childNode && !traverse(childNode as Element)) {\n return false;\n }\n }\n\n text += `</${nodeName}>`;\n }\n\n return true;\n };\n\n for (let i = 0; i < div.childNodes.length; i++) {\n const childNode = div.childNodes[i];\n\n if (childNode && !traverse(childNode as Element)) {\n return text;\n }\n }\n\n return text;\n};\n\nexport const getCharactersCount = (html: string): number => {\n const div = document.createElement('div');\n\n div.innerHTML = html;\n\n let count = 0;\n\n const traverse = (node: Node): void => {\n if (node.nodeName === 'TWIGNORE') {\n count += 1;\n } else if (node.nodeType === 3 && typeof node.textContent === 'string') {\n count += node.textContent.trim().length;\n } else if (node.nodeType === 1) {\n if (node.nodeName === 'CODE' && node.textContent !== null) {\n count += node.textContent.length;\n\n return;\n }\n\n Array.from(node.childNodes).forEach(traverse);\n }\n };\n\n Array.from(div.childNodes).forEach(traverse);\n\n return count;\n};\n\nexport const shuffleArray = <T>(array: T[]): T[] => {\n const result = Array.from(array);\n\n for (let i = result.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n\n [result[i], result[j]] = [result[j]!, result[i]!];\n }\n\n return result;\n};\n\ninterface CalculateAutoSpeedProps {\n fullTextLength: number;\n currentPosition: number;\n baseSpeedFactor: number;\n}\n\nexport const calculateAutoSpeed = ({\n fullTextLength,\n currentPosition,\n baseSpeedFactor,\n}: CalculateAutoSpeedProps): { speed: number; steps: number } => {\n const MIN_SPEED = 1;\n const MAX_SPEED = 10;\n\n const remainingLength = fullTextLength - currentPosition;\n\n // Calculate the speed with the remaining text length and the baseSpeedFactor\n const speed = Math.min(baseSpeedFactor / remainingLength, MAX_SPEED);\n\n if (speed < MIN_SPEED) {\n return { speed: 1, steps: 2 };\n }\n\n return { speed, steps: 1 };\n};\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"utils.js","names":["getSubTextFromHTML","html","length","div","document","createElement","innerHTML","text","currLength","escapeText","value","replace","escapeAttr","String","VOID_ELEMENTS","Set","traverse","node","nodeType","textContent","nodeText","remaining","substring","element","nodeName","toLowerCase","attributes","attribute","name","isVoid","has","i","childNodes","childNode","getCharactersCount","count","trim","Array","from","forEach","shuffleArray","array","result","j","Math","floor","random","calculateAutoSpeed","fullTextLength","currentPosition","baseSpeedFactor","MIN_SPEED","MAX_SPEED","remainingLength","speed","min","steps"],"sources":["../../../../src/components/typewriter/utils.ts"],"sourcesContent":["/**\n * Returns a substring of an HTML string while preserving HTML structure.\n *\n * Core rules:\n * - Element nodes are re-serialized as tags (start/end) to keep structure.\n * - Text nodes are always HTML-escaped on output. This prevents that previously\n * escaped text (like \"&lt;div&gt;\") turns into real tags during the DOM round trip.\n * - Attribute values are HTML-escaped on output.\n * - Void elements are serialized without closing tags.\n * - For TWIGNORE/TW-IGNORE elements, the innerHTML is passed through so that\n * their content (including real HTML) remains untouched.\n * - On early cutoff (once the length limit is reached), already opened tags are\n * properly closed to keep the result valid HTML.\n *\n * Note on length counting:\n * - The length is based on the decoded textContent length (as the DOM provides),\n * not on byte length nor escaped entity length. This mirrors how the text is perceived.\n *\n * @param html The input HTML string; may contain a mix of real HTML and already escaped HTML.\n * @param length The maximum number of text characters (based on textContent) to include.\n * @returns A valid HTML string containing up to the specified number of text characters,\n * preserving HTML tags and keeping escaped text escaped.\n */\nexport const getSubTextFromHTML = (html: string, length: number): string => {\n const div = document.createElement('div');\n\n div.innerHTML = html;\n\n let text = '';\n let currLength = 0;\n\n // Escape text node content to ensure that decoded \"<\" and \">\" do not become real tags.\n const escapeText = (value: string): string =>\n value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n\n // Escape attribute values safely.\n const escapeAttr = (value: string): string =>\n String(value)\n .replace(/&/g, '&amp;')\n .replace(/\"/g, '&quot;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;');\n\n // HTML void elements (must not have closing tags)\n const VOID_ELEMENTS = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n ]);\n\n // Traverses nodes and appends to \"text\".\n // Returns false to signal \"stop traversal\" once the length limit is reached.\n const traverse = (node: Node): boolean => {\n // Text node\n if (node.nodeType === 3 && typeof node.textContent === 'string') {\n const nodeText = node.textContent;\n const remaining = length - currLength;\n\n if (remaining <= 0) {\n return false;\n }\n\n if (nodeText.length <= remaining) {\n // Always escape text before writing to output\n text += escapeText(nodeText);\n currLength += nodeText.length;\n } else {\n // Cut the text and stop traversal\n text += escapeText(nodeText.substring(0, remaining));\n currLength += remaining;\n return false;\n }\n\n return true;\n }\n\n // Element node\n if (node.nodeType === 1) {\n const element = node as Element;\n\n // Pass-through for TWIGNORE/TW-IGNORE: keep their HTML as-is.\n if (element.nodeName === 'TWIGNORE' || element.nodeName === 'TW-IGNORE') {\n // element.innerHTML serializes children; escaped text stays escaped,\n // real HTML stays HTML — exactly what we want here.\n text += element.innerHTML;\n return true;\n }\n\n const nodeName = element.nodeName.toLowerCase();\n\n // Serialize attributes safely\n let attributes = '';\n // @ts-expect-error: attributes is a NodeListOf<Attr>\n // eslint-disable-next-line no-restricted-syntax\n for (const attribute of element.attributes) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/restrict-template-expressions,@typescript-eslint/no-unsafe-argument\n attributes += ` ${attribute.name}=\"${escapeAttr(attribute.value)}\"`;\n }\n\n // Open tag\n text += `<${nodeName}${attributes}>`;\n\n // Void elements: do not recurse children and do not emit a closing tag\n const isVoid = VOID_ELEMENTS.has(nodeName);\n if (!isVoid) {\n // Recurse through children until limit is reached\n for (let i = 0; i < element.childNodes.length; i++) {\n const childNode = element.childNodes[i];\n if (childNode && !traverse(childNode)) {\n // On early stop: close this tag to keep valid HTML, then bubble stop.\n text += `</${nodeName}>`;\n return false;\n }\n }\n\n // Close tag after all children\n text += `</${nodeName}>`;\n }\n\n return true;\n }\n\n // Other node types (comments, etc.) are ignored for text length\n return true;\n };\n\n // Traverse top-level children\n for (let i = 0; i < div.childNodes.length; i++) {\n const childNode = div.childNodes[i];\n if (childNode && !traverse(childNode)) {\n return text;\n }\n }\n\n return text;\n};\n\nexport const getCharactersCount = (html: string): number => {\n const div = document.createElement('div');\n\n div.innerHTML = html;\n\n let count = 0;\n\n const traverse = (node: Node): void => {\n if (node.nodeName === 'TWIGNORE') {\n count += 1;\n } else if (node.nodeType === 3 && typeof node.textContent === 'string') {\n count += node.textContent.trim().length;\n } else if (node.nodeType === 1) {\n if (node.nodeName === 'CODE' && node.textContent !== null) {\n count += node.textContent.length;\n\n return;\n }\n\n Array.from(node.childNodes).forEach(traverse);\n }\n };\n\n Array.from(div.childNodes).forEach(traverse);\n\n return count;\n};\n\nexport const shuffleArray = <T>(array: T[]): T[] => {\n const result = Array.from(array);\n\n for (let i = result.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n [result[i], result[j]] = [result[j]!, result[i]!];\n }\n\n return result;\n};\n\ninterface CalculateAutoSpeedProps {\n fullTextLength: number;\n currentPosition: number;\n baseSpeedFactor: number;\n}\n\nexport const calculateAutoSpeed = ({\n fullTextLength,\n currentPosition,\n baseSpeedFactor,\n}: CalculateAutoSpeedProps): { speed: number; steps: number } => {\n const MIN_SPEED = 1;\n const MAX_SPEED = 10;\n\n const remainingLength = fullTextLength - currentPosition;\n\n // Calculate the speed with the remaining text length and the baseSpeedFactor\n const speed = Math.min(baseSpeedFactor / remainingLength, MAX_SPEED);\n\n if (speed < MIN_SPEED) {\n return { speed: 1, steps: 2 };\n }\n\n return { speed, steps: 1 };\n};\n"],"mappings":"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","ignoreList":[]}
@@ -1,8 +1,10 @@
1
- import React, { FC } from 'react';
1
+ import { FC } from 'react';
2
+ import { CSSPropertiesWithVars } from 'styled-components/dist/types';
2
3
  type AnimatedTypewriterTextProps = {
3
4
  shouldHideCursor: boolean;
4
5
  shownText: string;
5
- textStyle?: React.CSSProperties;
6
+ textStyle?: CSSPropertiesWithVars;
7
+ shouldRemainSingleLine: boolean;
6
8
  };
7
9
  declare const AnimatedTypewriterText: FC<AnimatedTypewriterTextProps>;
8
10
  export default AnimatedTypewriterText;
@@ -1,9 +1,10 @@
1
- import React, { FC, ReactElement } from 'react';
1
+ import { FC, ReactElement } from 'react';
2
+ import { CSSPropertiesWithVars } from 'styled-components/dist/types';
2
3
  import { CursorType } from '../../types/cursor';
3
4
  import { TypewriterDelay, TypewriterSpeed } from '../../types/speed';
4
5
  export type TypewriterProps = {
5
6
  /**
6
- * The amount of characters that will be animated per animation cycle.
7
+ * The number of characters that will be animated per animation cycle.
7
8
  */
8
9
  animationSteps?: number;
9
10
  /**
@@ -70,6 +71,10 @@ export type TypewriterProps = {
70
71
  * Specifies whether the cursor should be hidden
71
72
  */
72
73
  shouldHideCursor?: boolean;
74
+ /**
75
+ * Whether the content should remain a single line.
76
+ */
77
+ shouldRemainSingleLine?: boolean;
73
78
  /**
74
79
  * Specifies whether the children should be sorted randomly if there are multiple texts.
75
80
  * This makes the typewriter start with a different text each time and also changes them
@@ -105,7 +110,7 @@ export type TypewriterProps = {
105
110
  /**
106
111
  * The style of the typewriter text element
107
112
  */
108
- textStyle?: React.CSSProperties;
113
+ textStyle?: CSSPropertiesWithVars;
109
114
  };
110
115
  declare const Typewriter: FC<TypewriterProps>;
111
116
  export default Typewriter;
@@ -14,6 +14,7 @@ type StyledTypewriterPseudoTextProps = WithTheme<{
14
14
  export declare const StyledTypewriterPseudoText: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, StyledTypewriterPseudoTextProps>> & string;
15
15
  type StyledTypewriterTextProps = WithTheme<{
16
16
  $isAnimatingText?: boolean;
17
+ $shouldRemainSingleLine: boolean;
17
18
  }>;
18
19
  export declare const StyledTypewriterText: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Substitute<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, StyledTypewriterTextProps>> & string;
19
20
  export {};
@@ -1,12 +1,25 @@
1
1
  /**
2
- * This function extracts a part of the text from an HTML text. The HTML elements themselves are
3
- * returned in the result. In addition, the function ensures that the closing tag of the Bold HTML
4
- * element is also returned for text that is cut off in the middle of a Bold element, for example.
2
+ * Returns a substring of an HTML string while preserving HTML structure.
5
3
  *
6
- * @param html - The text from which a part should be taken
7
- * @param length - The length of the text to be extracted
4
+ * Core rules:
5
+ * - Element nodes are re-serialized as tags (start/end) to keep structure.
6
+ * - Text nodes are always HTML-escaped on output. This prevents that previously
7
+ * escaped text (like "&lt;div&gt;") turns into real tags during the DOM round trip.
8
+ * - Attribute values are HTML-escaped on output.
9
+ * - Void elements are serialized without closing tags.
10
+ * - For TWIGNORE/TW-IGNORE elements, the innerHTML is passed through so that
11
+ * their content (including real HTML) remains untouched.
12
+ * - On early cutoff (once the length limit is reached), already opened tags are
13
+ * properly closed to keep the result valid HTML.
8
14
  *
9
- * @return string - The text part with the specified length - additionally the HTML elements are added
15
+ * Note on length counting:
16
+ * - The length is based on the decoded textContent length (as the DOM provides),
17
+ * not on byte length nor escaped entity length. This mirrors how the text is perceived.
18
+ *
19
+ * @param html The input HTML string; may contain a mix of real HTML and already escaped HTML.
20
+ * @param length The maximum number of text characters (based on textContent) to include.
21
+ * @returns A valid HTML string containing up to the specified number of text characters,
22
+ * preserving HTML tags and keeping escaped text escaped.
10
23
  */
11
24
  export declare const getSubTextFromHTML: (html: string, length: number) => string;
12
25
  export declare const getCharactersCount: (html: string) => number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chayns-components/typewriter",
3
- "version": "5.0.0-beta.997",
3
+ "version": "5.0.0",
4
4
  "description": "A set of beautiful React components for developing your own applications with chayns.",
5
5
  "sideEffects": false,
6
6
  "browserslist": [
@@ -23,8 +23,9 @@
23
23
  "exports": {
24
24
  ".": {
25
25
  "types": "./lib/types/index.d.ts",
26
+ "node": "./lib/cjs/index.js",
26
27
  "require": "./lib/cjs/index.js",
27
- "import": "./lib/esm/index.js"
28
+ "default": "./lib/esm/index.js"
28
29
  }
29
30
  },
30
31
  "directories": {
@@ -51,29 +52,29 @@
51
52
  "url": "https://github.com/TobitSoftware/chayns-components/issues"
52
53
  },
53
54
  "devDependencies": {
54
- "@babel/cli": "^7.26.4",
55
- "@babel/core": "^7.26.0",
56
- "@babel/preset-env": "^7.26.0",
57
- "@babel/preset-react": "^7.26.3",
58
- "@babel/preset-typescript": "^7.26.0",
59
- "@types/react": "^18.3.18",
60
- "@types/react-dom": "^18.3.5",
61
- "@types/styled-components": "^5.1.34",
55
+ "@babel/cli": "^7.28.6",
56
+ "@babel/core": "^7.29.0",
57
+ "@babel/preset-env": "^7.29.0",
58
+ "@babel/preset-react": "^7.28.5",
59
+ "@babel/preset-typescript": "^7.28.5",
60
+ "@types/react": "^18.3.28",
61
+ "@types/react-dom": "^18.3.7",
62
+ "@types/styled-components": "^5.1.36",
62
63
  "@types/uuid": "^10.0.0",
63
64
  "babel-loader": "^9.2.1",
64
65
  "cross-env": "^7.0.3",
65
- "lerna": "^8.1.9",
66
+ "lerna": "^8.2.4",
66
67
  "react": "^18.3.1",
67
68
  "react-dom": "^18.3.1",
68
- "styled-components": "^6.1.14",
69
- "typescript": "^5.7.3"
69
+ "styled-components": "^6.3.9",
70
+ "typescript": "^5.9.3"
70
71
  },
71
72
  "dependencies": {
72
- "@chayns-components/core": "^5.0.0-beta.997"
73
+ "@chayns-components/core": "^5.0.0"
73
74
  },
74
75
  "peerDependencies": {
75
- "chayns-api": ">=2.0.0",
76
- "framer-motion": ">=10.18.0",
76
+ "chayns-api": ">=2.2.0",
77
+ "motion": ">=12.4.1",
77
78
  "react": ">=18.0.0",
78
79
  "react-dom": ">=18.0.0",
79
80
  "styled-components": ">=5.3.11"
@@ -81,5 +82,5 @@
81
82
  "publishConfig": {
82
83
  "access": "public"
83
84
  },
84
- "gitHead": "88b9adc4186af824999d62f8a1a5e907fceebb06"
85
+ "gitHead": "3d554c67b058b3b25e2666a34ee543fea2cad6e2"
85
86
  }