@versini/ui-truncate 5.1.7 → 5.2.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.
package/dist/index.d.ts CHANGED
@@ -1,37 +1,37 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
-
3
- type TruncateProps = {
4
- /**
5
- * The children to render.
6
- */
7
- children: React.ReactNode;
8
- /**
9
- * The maximum length of the text.
10
- * @default 200
11
- */
12
- length?: number;
13
- /**
14
- * Opt-in flag to enable truncation of rich React nodes (HTML / nested elements).
15
- * When false (default), only plain string children are truncated; non-string
16
- * children are rendered as-is. When true, the component will traverse the
17
- * React node tree, measure the concatenated textual content, and rebuild a
18
- * structurally equivalent subtree truncated at the calculated boundary.
19
- * @default false
20
- */
21
- enableRichTruncation?: boolean;
22
- /**
23
- * The mode of Button. This will change the color of the Button.
24
- * @default "system"
25
- */
26
- mode?: "dark" | "light" | "system" | "alt-system";
27
- /**
28
- * The type of focus for the Button. This will change the color
29
- * of the focus ring around the Button.
30
- * @default "system"
31
- */
32
- focusMode?: "dark" | "light" | "system" | "alt-system";
33
- };
34
-
35
- declare const Truncate: ({ children, length, mode, focusMode, enableRichTruncation, }: TruncateProps) => react_jsx_runtime.JSX.Element;
36
-
37
- export { Truncate };
1
+ import { JSX } from 'react/jsx-runtime';
2
+
3
+ export declare const Truncate: ({ children, length, mode, focusMode, enableRichTruncation, }: TruncateProps) => JSX.Element;
4
+
5
+ declare type TruncateProps = {
6
+ /**
7
+ * The children to render.
8
+ */
9
+ children: React.ReactNode;
10
+ /**
11
+ * The maximum length of the text.
12
+ * @default 200
13
+ */
14
+ length?: number;
15
+ /**
16
+ * Opt-in flag to enable truncation of rich React nodes (HTML / nested elements).
17
+ * When false (default), only plain string children are truncated; non-string
18
+ * children are rendered as-is. When true, the component will traverse the
19
+ * React node tree, measure the concatenated textual content, and rebuild a
20
+ * structurally equivalent subtree truncated at the calculated boundary.
21
+ * @default false
22
+ */
23
+ enableRichTruncation?: boolean;
24
+ /**
25
+ * The mode of Button. This will change the color of the Button.
26
+ * @default "system"
27
+ */
28
+ mode?: "dark" | "light" | "system" | "alt-system";
29
+ /**
30
+ * The type of focus for the Button. This will change the color
31
+ * of the focus ring around the Button.
32
+ * @default "system"
33
+ */
34
+ focusMode?: "dark" | "light" | "system" | "alt-system";
35
+ };
36
+
37
+ export { }
package/dist/index.js CHANGED
@@ -1,17 +1,212 @@
1
- import { Truncate as o } from "./components/Truncate/Truncate.js";
2
1
  /*!
3
- @versini/ui-truncate v5.1.7
2
+ @versini/ui-truncate v5.2.0
4
3
  © 2025 gizmette.com
5
4
  */
6
5
  try {
7
- window.__VERSINI_UI_TRUNCATE__ || (window.__VERSINI_UI_TRUNCATE__ = {
8
- version: "5.1.7",
9
- buildTime: "11/04/2025 09:51 AM EST",
10
- homepage: "https://github.com/aversini/ui-components",
11
- license: "MIT"
12
- });
13
- } catch {
6
+ if (!window.__VERSINI_UI_TRUNCATE__) {
7
+ window.__VERSINI_UI_TRUNCATE__ = {
8
+ version: "5.2.0",
9
+ buildTime: "11/04/2025 03:45 PM EST",
10
+ homepage: "https://github.com/aversini/ui-components",
11
+ license: "MIT",
12
+ };
13
+ }
14
+ } catch (error) {
15
+ // nothing to declare officer
14
16
  }
15
- export {
16
- o as Truncate
17
+
18
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
19
+ import { Button } from "@versini/ui-button";
20
+ import { isValidElement, useMemo, useState } from "react";
21
+
22
+ ;// CONCATENATED MODULE: external "react/jsx-runtime"
23
+
24
+ ;// CONCATENATED MODULE: external "@versini/ui-button"
25
+
26
+ ;// CONCATENATED MODULE: external "react"
27
+
28
+ ;// CONCATENATED MODULE: ./src/components/Truncate/utilities.ts
29
+ const DEFAULT_LENGTH = 200;
30
+ /**
31
+ * This function will truncate the string at the last word boundary
32
+ * before the ideal length.
33
+ * - If ideal length ends up in the middle of a word, truncate at the next space.
34
+ * - If ideal length ends up on a space, truncate at the ideal length.
35
+ */ const truncate = ({ string, idealLength = DEFAULT_LENGTH })=>{
36
+ const strLength = string.length;
37
+ if (strLength <= idealLength) {
38
+ return {
39
+ string: string,
40
+ isTruncated: false
41
+ };
42
+ }
43
+ const originalTrunc = string.charAt(idealLength);
44
+ if (originalTrunc === " ") {
45
+ return {
46
+ string: string.slice(0, idealLength),
47
+ isTruncated: true
48
+ };
49
+ }
50
+ const nextSpace = string.slice(idealLength).search(" ");
51
+ return {
52
+ string: string.slice(0, idealLength + nextSpace),
53
+ isTruncated: true
54
+ };
17
55
  };
56
+
57
+ ;// CONCATENATED MODULE: ./src/components/Truncate/Truncate.tsx
58
+
59
+
60
+
61
+
62
+ /**
63
+ * Recursively extract concatenated textual content from arbitrary React nodes.
64
+ * This is defined outside the component to keep a stable reference and avoid
65
+ * unnecessary re-creation on each render.
66
+ *
67
+ * NOTE: we are keeping this function here instead of utilities because it
68
+ * relies on React.
69
+ *
70
+ */ const extractText = (node)=>{
71
+ if (node == null || typeof node === "boolean") {
72
+ return "";
73
+ }
74
+ if (typeof node === "string" || typeof node === "number") {
75
+ return String(node);
76
+ }
77
+ if (Array.isArray(node)) {
78
+ return node.map(extractText).join("");
79
+ }
80
+ if (/*#__PURE__*/ isValidElement(node)) {
81
+ return extractText(node.props.children);
82
+ }
83
+ /* c8 ignore next 1 */ return ""; // Unsupported node type fallback
84
+ };
85
+ /**
86
+ * Block-level HTML tag detection (upper-case because React string element types
87
+ * are lowercase).
88
+ */ const BLOCK_TAGS = new Set([
89
+ "P",
90
+ "DIV",
91
+ "UL",
92
+ "OL",
93
+ "LI",
94
+ "SECTION",
95
+ "ARTICLE",
96
+ "HEADER",
97
+ "FOOTER",
98
+ "MAIN",
99
+ "ASIDE",
100
+ "TABLE",
101
+ "THEAD",
102
+ "TBODY",
103
+ "TFOOT",
104
+ "TR",
105
+ "TD",
106
+ "TH",
107
+ "BLOCKQUOTE",
108
+ "PRE",
109
+ "H1",
110
+ "H2",
111
+ "H3",
112
+ "H4",
113
+ "H5",
114
+ "H6"
115
+ ]);
116
+ /**
117
+ * Detect whether a React node tree contains any block-level HTML elements. Used
118
+ * to decide if inline spacing (margin-left) should be suppressed when expanded
119
+ * rich content is displayed on its own line(s).
120
+ */ const hasBlockLike = (node)=>{
121
+ if (node == null || typeof node === "boolean") {
122
+ return false;
123
+ }
124
+ if (typeof node === "string" || typeof node === "number") {
125
+ return false;
126
+ }
127
+ if (Array.isArray(node)) {
128
+ return node.some(hasBlockLike);
129
+ }
130
+ if (/*#__PURE__*/ isValidElement(node)) {
131
+ const el = node;
132
+ if (typeof el.type === "string" && BLOCK_TAGS.has(el.type.toUpperCase())) {
133
+ return true;
134
+ }
135
+ return hasBlockLike(el.props.children);
136
+ }
137
+ /* c8 ignore next 1 */ return false; // Unsupported node types treated as non-block for spacing
138
+ };
139
+ const Truncate = ({ children, length = 200, mode = "system", focusMode = "system", enableRichTruncation = false })=>{
140
+ const [isExpanded, setIsExpanded] = useState(false);
141
+ const nonStringBypass = typeof children !== "string" && !enableRichTruncation;
142
+ const fullText = useMemo(()=>{
143
+ return typeof children === "string" ? children : extractText(children);
144
+ }, [
145
+ children
146
+ ]);
147
+ const { string: truncatedText, isTruncated } = useMemo(()=>{
148
+ return truncate({
149
+ string: fullText,
150
+ idealLength: length
151
+ });
152
+ }, [
153
+ fullText,
154
+ length
155
+ ]);
156
+ /**
157
+ * We flatten rich truncated output to raw text so styling doesn't cause the
158
+ * toggle button to wrap onto a new line.
159
+ */ const showPlainTruncated = enableRichTruncation && isTruncated && !isExpanded;
160
+ const handleToggleExpanded = (e)=>{
161
+ e.preventDefault();
162
+ setIsExpanded(!isExpanded);
163
+ };
164
+ if (nonStringBypass) {
165
+ return /*#__PURE__*/ jsx(Fragment, {
166
+ children: children
167
+ });
168
+ }
169
+ let displayContent;
170
+ if (!isTruncated) {
171
+ displayContent = children; // Nothing to truncate
172
+ } else if (isExpanded) {
173
+ displayContent = children; // Show full rich content
174
+ } else if (showPlainTruncated) {
175
+ displayContent = truncatedText; // Plain flattened version
176
+ } else {
177
+ displayContent = truncatedText; // String case
178
+ }
179
+ /**
180
+ * Spacing strategy:
181
+ * - Always keep margin for classic string truncation (maintains original behavior).
182
+ * - Keep margin for collapsed flattened rich text (button inline with plain snippet).
183
+ * - When expanded AND rich truncation enabled: remove margin ONLY if the rich content
184
+ * tree actually contains a block-level element that likely puts the button on a new line.
185
+ */ const contentHasBlock = enableRichTruncation ? hasBlockLike(children) : false;
186
+ const removeMargin = enableRichTruncation && isExpanded && contentHasBlock;
187
+ const needsInlineSpacing = !removeMargin;
188
+ return /*#__PURE__*/ jsxs("span", {
189
+ style: {
190
+ wordBreak: "break-word"
191
+ },
192
+ "data-testid": "truncate-root",
193
+ "aria-expanded": isTruncated ? isExpanded : undefined,
194
+ children: [
195
+ displayContent,
196
+ isTruncated && /*#__PURE__*/ jsx(Button, {
197
+ mode: mode,
198
+ focusMode: focusMode,
199
+ className: needsInlineSpacing ? "ml-2" : undefined,
200
+ size: "small",
201
+ onClick: handleToggleExpanded,
202
+ "aria-label": isExpanded ? "Show less" : "Show more",
203
+ children: isExpanded ? "less..." : "more..."
204
+ })
205
+ ]
206
+ });
207
+ };
208
+
209
+ ;// CONCATENATED MODULE: ./src/components/index.ts
210
+
211
+
212
+ export { Truncate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-truncate",
3
- "version": "5.1.7",
3
+ "version": "5.2.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -20,13 +20,13 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "build:check": "tsc",
23
- "build:js": "vite build",
24
- "build:types": "tsup",
25
- "build": "npm-run-all --serial clean build:check build:js build:types",
23
+ "build:js": "rslib build",
24
+ "build:types": "echo 'Types now built with rslib'",
25
+ "build": "npm-run-all --serial clean build:check build:js",
26
26
  "clean": "rimraf dist tmp",
27
- "dev:js": "vite build --watch --mode development",
28
- "dev:types": "tsup --watch src",
29
- "dev": "npm-run-all clean --parallel dev:js dev:types",
27
+ "dev:js": "rslib build --watch",
28
+ "dev:types": "echo 'Types now watched with rslib'",
29
+ "dev": "rslib build --watch",
30
30
  "lint": "biome lint src",
31
31
  "lint:fix": "biome check src --write --no-errors-on-unmatched",
32
32
  "prettier": "biome check --write --no-errors-on-unmatched",
@@ -39,11 +39,11 @@
39
39
  "dependencies": {
40
40
  "@tailwindcss/typography": "0.5.19",
41
41
  "@testing-library/jest-dom": "6.9.1",
42
- "@versini/ui-button": "8.2.0",
42
+ "@versini/ui-button": "8.3.1",
43
43
  "tailwindcss": "4.1.16"
44
44
  },
45
45
  "sideEffects": [
46
46
  "**/*.css"
47
47
  ],
48
- "gitHead": "d43a776558072976e0920ab60f28f45a9efd3848"
48
+ "gitHead": "7484ad443b77ef31e52ae3a7d88b8129bc6cdf1d"
49
49
  }
@@ -1,95 +0,0 @@
1
- import { jsx as p, Fragment as H, jsxs as O } from "react/jsx-runtime";
2
- import { Button as B } from "@versini/ui-button";
3
- import { useState as I, useMemo as T, isValidElement as m } from "react";
4
- const L = 200, D = ({
5
- string: t,
6
- idealLength: e = L
7
- }) => {
8
- if (t.length <= e)
9
- return { string: t, isTruncated: !1 };
10
- if (t.charAt(e) === " ")
11
- return { string: t.slice(0, e), isTruncated: !0 };
12
- const s = t.slice(e).search(" ");
13
- return {
14
- string: t.slice(0, e + s),
15
- isTruncated: !0
16
- };
17
- }, o = (t) => t == null || typeof t == "boolean" ? "" : typeof t == "string" || typeof t == "number" ? String(t) : Array.isArray(t) ? t.map(o).join("") : m(t) ? o(t.props.children) : "", C = /* @__PURE__ */ new Set([
18
- "P",
19
- "DIV",
20
- "UL",
21
- "OL",
22
- "LI",
23
- "SECTION",
24
- "ARTICLE",
25
- "HEADER",
26
- "FOOTER",
27
- "MAIN",
28
- "ASIDE",
29
- "TABLE",
30
- "THEAD",
31
- "TBODY",
32
- "TFOOT",
33
- "TR",
34
- "TD",
35
- "TH",
36
- "BLOCKQUOTE",
37
- "PRE",
38
- "H1",
39
- "H2",
40
- "H3",
41
- "H4",
42
- "H5",
43
- "H6"
44
- ]), i = (t) => {
45
- if (t == null || typeof t == "boolean" || typeof t == "string" || typeof t == "number")
46
- return !1;
47
- if (Array.isArray(t))
48
- return t.some(i);
49
- if (m(t)) {
50
- const e = t;
51
- return typeof e.type == "string" && C.has(e.type.toUpperCase()) ? !0 : i(e.props.children);
52
- }
53
- return !1;
54
- }, N = ({
55
- children: t,
56
- length: e = 200,
57
- mode: u = "system",
58
- focusMode: f = "system",
59
- enableRichTruncation: s = !1
60
- }) => {
61
- const [r, y] = I(!1), E = typeof t != "string" && !s, l = T(() => typeof t == "string" ? t : o(t), [t]), { string: c, isTruncated: a } = T(() => D({ string: l, idealLength: e }), [l, e]), g = s && a && !r, A = (S) => {
62
- S.preventDefault(), y(!r);
63
- };
64
- if (E)
65
- return /* @__PURE__ */ p(H, { children: t });
66
- let n;
67
- a ? r ? n = t : n = c : n = t;
68
- const x = s ? i(t) : !1;
69
- return /* @__PURE__ */ O(
70
- "span",
71
- {
72
- style: { wordBreak: "break-word" },
73
- "data-testid": "truncate-root",
74
- "aria-expanded": a ? r : void 0,
75
- children: [
76
- n,
77
- a && /* @__PURE__ */ p(
78
- B,
79
- {
80
- mode: u,
81
- focusMode: f,
82
- className: !(s && r && x) ? "ml-2" : void 0,
83
- size: "small",
84
- onClick: A,
85
- "aria-label": r ? "Show less" : "Show more",
86
- children: r ? "less..." : "more..."
87
- }
88
- )
89
- ]
90
- }
91
- );
92
- };
93
- export {
94
- N as Truncate
95
- };