@djangocfg/ui-tools 2.1.94 → 2.1.96

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/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @djangocfg/ui-tools
2
+
3
+ Heavy React tools with lazy loading (React.lazy + Suspense).
4
+
5
+ **No Next.js dependencies** — works with Electron, Vite, CRA, and any React environment.
6
+
7
+ **Part of [DjangoCFG](https://djangocfg.com)** — modern Django framework for production-ready SaaS applications.
8
+
9
+ **[Live Demo & Props](https://djangocfg.com/demo/)**
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pnpm add @djangocfg/ui-tools
15
+ ```
16
+
17
+ ## Why ui-tools?
18
+
19
+ This package contains heavy components that are loaded lazily to keep your initial bundle small. Each tool is loaded only when used.
20
+
21
+ | Package | Use Case |
22
+ |---------|----------|
23
+ | `@djangocfg/ui-core` | Lightweight UI components (60 components) |
24
+ | `@djangocfg/ui-tools` | Heavy tools with lazy loading |
25
+ | `@djangocfg/ui-nextjs` | Next.js apps (extends ui-core) |
26
+
27
+ ## Tools (9)
28
+
29
+ | Tool | Bundle Size | Description |
30
+ |------|-------------|-------------|
31
+ | `Mermaid` | ~800KB | Diagram rendering |
32
+ | `PrettyCode` | ~500KB | Code syntax highlighting |
33
+ | `OpenapiViewer` | ~400KB | OpenAPI schema viewer & playground |
34
+ | `JsonForm` | ~300KB | JSON Schema form generator |
35
+ | `LottiePlayer` | ~200KB | Lottie animation player |
36
+ | `AudioPlayer` | ~200KB | Audio player with WaveSurfer.js |
37
+ | `VideoPlayer` | ~150KB | Professional video player with Vidstack |
38
+ | `JsonTree` | ~100KB | JSON visualization |
39
+ | `ImageViewer` | ~50KB | Image viewer with zoom/pan/rotate |
40
+
41
+ ## Components
42
+
43
+ | Component | Description |
44
+ |-----------|-------------|
45
+ | `Markdown` | Markdown renderer with GFM support |
46
+
47
+ ## Stores
48
+
49
+ | Store | Description |
50
+ |-------|-------------|
51
+ | `useMediaCacheStore` | Media caching for video/audio players |
52
+
53
+ ## Usage
54
+
55
+ ```tsx
56
+ import { VideoPlayer, AudioPlayer, Mermaid } from '@djangocfg/ui-tools';
57
+ import { Markdown } from '@djangocfg/ui-tools';
58
+
59
+ function Example() {
60
+ return (
61
+ <>
62
+ <VideoPlayer src="https://example.com/video.mp4" />
63
+ <AudioPlayer src="https://example.com/audio.mp3" />
64
+ <Mermaid chart={`graph TD; A-->B;`} />
65
+ <Markdown content="# Hello **World**" />
66
+ </>
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## JSON Form Example
72
+
73
+ ```tsx
74
+ import { JsonForm } from '@djangocfg/ui-tools';
75
+
76
+ const schema = {
77
+ type: 'object',
78
+ properties: {
79
+ name: { type: 'string', title: 'Name' },
80
+ email: { type: 'string', format: 'email', title: 'Email' },
81
+ },
82
+ };
83
+
84
+ function Form() {
85
+ return (
86
+ <JsonForm
87
+ schema={schema}
88
+ onSubmit={(data) => console.log(data)}
89
+ />
90
+ );
91
+ }
92
+ ```
93
+
94
+ ## Code Highlighting Example
95
+
96
+ ```tsx
97
+ import { PrettyCode } from '@djangocfg/ui-tools';
98
+
99
+ function CodeBlock() {
100
+ return (
101
+ <PrettyCode
102
+ code={`const hello = "world";`}
103
+ language="typescript"
104
+ />
105
+ );
106
+ }
107
+ ```
108
+
109
+ ## Lazy Loading
110
+
111
+ All tools use React.lazy + Suspense for automatic code splitting:
112
+
113
+ ```tsx
114
+ // Each tool is loaded only when rendered
115
+ <Suspense fallback={<Spinner />}>
116
+ <Mermaid chart={diagram} />
117
+ </Suspense>
118
+ ```
119
+
120
+ ## Requirements
121
+
122
+ - React >= 18 or >= 19
123
+ - Tailwind CSS >= 4
124
+ - Zustand >= 5
125
+ - @djangocfg/ui-core (peer dependency)
126
+
127
+ ---
128
+
129
+ **[Full documentation & examples](https://djangocfg.com/demo/)**
package/dist/index.cjs CHANGED
@@ -4763,6 +4763,121 @@ function ImageViewer({ file, content, src: directSrc, inDialog = false }) {
4763
4763
  );
4764
4764
  }
4765
4765
  chunkUQ3XI5MY_cjs.__name(ImageViewer, "ImageViewer");
4766
+ function smartTruncate(content, maxLength) {
4767
+ if (content.length <= maxLength) {
4768
+ return content;
4769
+ }
4770
+ let breakPoint = maxLength;
4771
+ while (breakPoint > maxLength - 50 && breakPoint > 0) {
4772
+ const char = content[breakPoint];
4773
+ if (char === " " || char === "\n" || char === " ") {
4774
+ break;
4775
+ }
4776
+ breakPoint--;
4777
+ }
4778
+ if (breakPoint <= maxLength - 50) {
4779
+ breakPoint = maxLength;
4780
+ }
4781
+ let truncated = content.slice(0, breakPoint).trimEnd();
4782
+ truncated = fixUnclosedMarkdown(truncated);
4783
+ return truncated;
4784
+ }
4785
+ chunkUQ3XI5MY_cjs.__name(smartTruncate, "smartTruncate");
4786
+ function truncateByLines(content, maxLines) {
4787
+ const lines = content.split("\n");
4788
+ if (lines.length <= maxLines) {
4789
+ return content;
4790
+ }
4791
+ let truncated = lines.slice(0, maxLines).join("\n").trimEnd();
4792
+ truncated = fixUnclosedMarkdown(truncated);
4793
+ return truncated;
4794
+ }
4795
+ chunkUQ3XI5MY_cjs.__name(truncateByLines, "truncateByLines");
4796
+ function fixUnclosedMarkdown(content) {
4797
+ let result = content;
4798
+ const countOccurrences = /* @__PURE__ */ chunkUQ3XI5MY_cjs.__name((str, marker) => {
4799
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4800
+ const matches = str.match(new RegExp(escaped, "g"));
4801
+ return matches ? matches.length : 0;
4802
+ }, "countOccurrences");
4803
+ const boldCount = countOccurrences(result, "**");
4804
+ if (boldCount % 2 !== 0) {
4805
+ result += "**";
4806
+ }
4807
+ const withoutBold = result.replace(/\*\*/g, "");
4808
+ const italicCount = countOccurrences(withoutBold, "*");
4809
+ if (italicCount % 2 !== 0) {
4810
+ result += "*";
4811
+ }
4812
+ const codeCount = countOccurrences(result, "`");
4813
+ const tripleCount = countOccurrences(result, "```");
4814
+ const singleCodeCount = codeCount - tripleCount * 3;
4815
+ if (singleCodeCount % 2 !== 0) {
4816
+ result += "`";
4817
+ }
4818
+ if (tripleCount % 2 !== 0) {
4819
+ result += "\n```";
4820
+ }
4821
+ const strikeCount = countOccurrences(result, "~~");
4822
+ if (strikeCount % 2 !== 0) {
4823
+ result += "~~";
4824
+ }
4825
+ const underlineBoldCount = countOccurrences(result, "__");
4826
+ if (underlineBoldCount % 2 !== 0) {
4827
+ result += "__";
4828
+ }
4829
+ const withoutUnderlineBold = result.replace(/__/g, "");
4830
+ const underlineItalicCount = countOccurrences(withoutUnderlineBold, "_");
4831
+ if (underlineItalicCount % 2 !== 0) {
4832
+ result += "_";
4833
+ }
4834
+ return result;
4835
+ }
4836
+ chunkUQ3XI5MY_cjs.__name(fixUnclosedMarkdown, "fixUnclosedMarkdown");
4837
+ function useCollapsibleContent(content, options = {}) {
4838
+ const { maxLength, maxLines, defaultExpanded = false } = options;
4839
+ const [isCollapsed, setIsCollapsed] = React17.useState(!defaultExpanded);
4840
+ const originalLength = content.length;
4841
+ const originalLineCount = content.split("\n").length;
4842
+ const { shouldCollapse, truncatedContent } = React17.useMemo(() => {
4843
+ if (maxLength === void 0 && maxLines === void 0) {
4844
+ return { shouldCollapse: false, truncatedContent: content };
4845
+ }
4846
+ let needsCollapse = false;
4847
+ let result = content;
4848
+ if (maxLines !== void 0 && originalLineCount > maxLines) {
4849
+ needsCollapse = true;
4850
+ result = truncateByLines(result, maxLines);
4851
+ }
4852
+ if (maxLength !== void 0 && result.length > maxLength) {
4853
+ needsCollapse = true;
4854
+ result = smartTruncate(result, maxLength);
4855
+ }
4856
+ return { shouldCollapse: needsCollapse, truncatedContent: result };
4857
+ }, [content, maxLength, maxLines, originalLineCount]);
4858
+ const displayContent = React17.useMemo(() => {
4859
+ if (!shouldCollapse || !isCollapsed) {
4860
+ return content;
4861
+ }
4862
+ return truncatedContent;
4863
+ }, [content, truncatedContent, shouldCollapse, isCollapsed]);
4864
+ const toggleCollapsed = React17.useCallback(() => {
4865
+ setIsCollapsed((prev) => !prev);
4866
+ }, []);
4867
+ const setCollapsed = React17.useCallback((collapsed) => {
4868
+ setIsCollapsed(collapsed);
4869
+ }, []);
4870
+ return {
4871
+ isCollapsed,
4872
+ toggleCollapsed,
4873
+ setCollapsed,
4874
+ displayContent,
4875
+ shouldCollapse,
4876
+ originalLength,
4877
+ originalLineCount
4878
+ };
4879
+ }
4880
+ chunkUQ3XI5MY_cjs.__name(useCollapsibleContent, "useCollapsibleContent");
4766
4881
  var extractTextFromChildren = /* @__PURE__ */ chunkUQ3XI5MY_cjs.__name((children) => {
4767
4882
  if (typeof children === "string") {
4768
4883
  return children;
@@ -4947,46 +5062,166 @@ var hasMarkdownSyntax = /* @__PURE__ */ chunkUQ3XI5MY_cjs.__name((text) => {
4947
5062
  ];
4948
5063
  return markdownPatterns.some((pattern) => pattern.test(text));
4949
5064
  }, "hasMarkdownSyntax");
5065
+ var CollapseToggle = /* @__PURE__ */ chunkUQ3XI5MY_cjs.__name(({
5066
+ isCollapsed,
5067
+ onClick,
5068
+ readMoreLabel,
5069
+ showLessLabel,
5070
+ isUser,
5071
+ isCompact
5072
+ }) => {
5073
+ const textSize = isCompact ? "text-xs" : "text-sm";
5074
+ return /* @__PURE__ */ jsxRuntime.jsx(
5075
+ "button",
5076
+ {
5077
+ type: "button",
5078
+ onClick,
5079
+ className: `
5080
+ ${textSize} font-medium cursor-pointer
5081
+ transition-colors duration-200
5082
+ ${isUser ? "text-white/80 hover:text-white" : "text-primary hover:text-primary/80"}
5083
+ inline-flex items-center gap-1
5084
+ mt-1
5085
+ `,
5086
+ children: isCollapsed ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5087
+ readMoreLabel,
5088
+ /* @__PURE__ */ jsxRuntime.jsx(
5089
+ "svg",
5090
+ {
5091
+ className: "w-3 h-3",
5092
+ fill: "none",
5093
+ stroke: "currentColor",
5094
+ viewBox: "0 0 24 24",
5095
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5096
+ "path",
5097
+ {
5098
+ strokeLinecap: "round",
5099
+ strokeLinejoin: "round",
5100
+ strokeWidth: 2,
5101
+ d: "M19 9l-7 7-7-7"
5102
+ }
5103
+ )
5104
+ }
5105
+ )
5106
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5107
+ showLessLabel,
5108
+ /* @__PURE__ */ jsxRuntime.jsx(
5109
+ "svg",
5110
+ {
5111
+ className: "w-3 h-3",
5112
+ fill: "none",
5113
+ stroke: "currentColor",
5114
+ viewBox: "0 0 24 24",
5115
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5116
+ "path",
5117
+ {
5118
+ strokeLinecap: "round",
5119
+ strokeLinejoin: "round",
5120
+ strokeWidth: 2,
5121
+ d: "M5 15l7-7 7 7"
5122
+ }
5123
+ )
5124
+ }
5125
+ )
5126
+ ] })
5127
+ }
5128
+ );
5129
+ }, "CollapseToggle");
4950
5130
  var MarkdownMessage = /* @__PURE__ */ chunkUQ3XI5MY_cjs.__name(({
4951
5131
  content,
4952
5132
  className = "",
4953
5133
  isUser = false,
4954
- isCompact = false
5134
+ isCompact = false,
5135
+ collapsible = false,
5136
+ maxLength,
5137
+ maxLines,
5138
+ readMoreLabel = "Read more...",
5139
+ showLessLabel = "Show less",
5140
+ defaultExpanded = false,
5141
+ onCollapseChange
4955
5142
  }) => {
4956
5143
  const trimmedContent = content.trim();
5144
+ const collapsibleOptions = React17__default.default.useMemo(() => {
5145
+ if (!collapsible) return {};
5146
+ const effectiveMaxLength = maxLength ?? 1e3;
5147
+ const effectiveMaxLines = maxLines ?? 10;
5148
+ return { maxLength: effectiveMaxLength, maxLines: effectiveMaxLines, defaultExpanded };
5149
+ }, [collapsible, maxLength, maxLines, defaultExpanded]);
5150
+ const {
5151
+ isCollapsed,
5152
+ toggleCollapsed,
5153
+ displayContent,
5154
+ shouldCollapse
5155
+ } = useCollapsibleContent(
5156
+ trimmedContent,
5157
+ collapsible ? collapsibleOptions : {}
5158
+ );
5159
+ React17__default.default.useEffect(() => {
5160
+ if (collapsible && shouldCollapse && onCollapseChange) {
5161
+ onCollapseChange(isCollapsed);
5162
+ }
5163
+ }, [isCollapsed, collapsible, shouldCollapse, onCollapseChange]);
4957
5164
  const components = React17__default.default.useMemo(() => createMarkdownComponents(isUser, isCompact), [isUser, isCompact]);
4958
5165
  const textSizeClass = isCompact ? "text-xs" : "text-sm";
4959
5166
  const proseClass = isCompact ? "prose-xs" : "prose-sm";
4960
- const isPlainText = !hasMarkdownSyntax(trimmedContent);
5167
+ const isPlainText = !hasMarkdownSyntax(displayContent);
4961
5168
  if (isPlainText) {
4962
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: `${textSizeClass} leading-relaxed break-words ${className}`, children: trimmedContent });
5169
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `${textSizeClass} leading-relaxed break-words ${className}`, children: [
5170
+ displayContent,
5171
+ collapsible && shouldCollapse && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5172
+ isCollapsed && "... ",
5173
+ /* @__PURE__ */ jsxRuntime.jsx(
5174
+ CollapseToggle,
5175
+ {
5176
+ isCollapsed,
5177
+ onClick: toggleCollapsed,
5178
+ readMoreLabel,
5179
+ showLessLabel,
5180
+ isUser,
5181
+ isCompact
5182
+ }
5183
+ )
5184
+ ] })
5185
+ ] });
4963
5186
  }
4964
- return /* @__PURE__ */ jsxRuntime.jsx(
4965
- "div",
4966
- {
4967
- className: `
4968
- prose ${proseClass} max-w-none break-words overflow-hidden ${textSizeClass}
4969
- ${isUser ? "prose-invert" : "dark:prose-invert"}
4970
- ${className}
4971
- `,
4972
- style: {
4973
- // Inherit colors from parent - fixes issues with external CSS variables
4974
- "--tw-prose-body": "inherit",
4975
- "--tw-prose-headings": "inherit",
4976
- "--tw-prose-bold": "inherit",
4977
- "--tw-prose-links": "inherit",
4978
- color: "inherit"
4979
- },
4980
- children: /* @__PURE__ */ jsxRuntime.jsx(
4981
- ReactMarkdown__default.default,
4982
- {
4983
- remarkPlugins: [remarkGfm__default.default],
4984
- components,
4985
- children: trimmedContent
4986
- }
4987
- )
4988
- }
4989
- );
5187
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, children: [
5188
+ /* @__PURE__ */ jsxRuntime.jsx(
5189
+ "div",
5190
+ {
5191
+ className: `
5192
+ prose ${proseClass} max-w-none break-words overflow-hidden ${textSizeClass}
5193
+ ${isUser ? "prose-invert" : "dark:prose-invert"}
5194
+ `,
5195
+ style: {
5196
+ // Inherit colors from parent - fixes issues with external CSS variables
5197
+ "--tw-prose-body": "inherit",
5198
+ "--tw-prose-headings": "inherit",
5199
+ "--tw-prose-bold": "inherit",
5200
+ "--tw-prose-links": "inherit",
5201
+ color: "inherit"
5202
+ },
5203
+ children: /* @__PURE__ */ jsxRuntime.jsx(
5204
+ ReactMarkdown__default.default,
5205
+ {
5206
+ remarkPlugins: [remarkGfm__default.default],
5207
+ components,
5208
+ children: displayContent
5209
+ }
5210
+ )
5211
+ }
5212
+ ),
5213
+ collapsible && shouldCollapse && /* @__PURE__ */ jsxRuntime.jsx(
5214
+ CollapseToggle,
5215
+ {
5216
+ isCollapsed,
5217
+ onClick: toggleCollapsed,
5218
+ readMoreLabel,
5219
+ showLessLabel,
5220
+ isUser,
5221
+ isCompact
5222
+ }
5223
+ )
5224
+ ] });
4990
5225
  }, "MarkdownMessage");
4991
5226
 
4992
5227
  Object.defineProperty(exports, "useLottie", {
@@ -5066,6 +5301,7 @@ exports.safeJsonStringify = safeJsonStringify;
5066
5301
  exports.useAudioCache = useAudioCache;
5067
5302
  exports.useAudioVisualization = useAudioVisualization;
5068
5303
  exports.useBlobUrlCleanup = useBlobUrlCleanup;
5304
+ exports.useCollapsibleContent = useCollapsibleContent;
5069
5305
  exports.useHybridAudio = useHybridAudio;
5070
5306
  exports.useHybridAudioAnalysis = useHybridAudioAnalysis;
5071
5307
  exports.useHybridAudioContext = useHybridAudioContext;