@djangocfg/ui-tools 2.1.397 → 2.1.400

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.397",
3
+ "version": "2.1.400",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -154,8 +154,8 @@
154
154
  "test:watch": "vitest"
155
155
  },
156
156
  "peerDependencies": {
157
- "@djangocfg/i18n": "^2.1.397",
158
- "@djangocfg/ui-core": "^2.1.397",
157
+ "@djangocfg/i18n": "^2.1.400",
158
+ "@djangocfg/ui-core": "^2.1.400",
159
159
  "consola": "^3.4.2",
160
160
  "lodash-es": "^4.18.1",
161
161
  "lucide-react": "^0.545.0",
@@ -209,9 +209,9 @@
209
209
  "material-file-icons": "^2.4.0"
210
210
  },
211
211
  "devDependencies": {
212
- "@djangocfg/i18n": "^2.1.397",
213
- "@djangocfg/typescript-config": "^2.1.397",
214
- "@djangocfg/ui-core": "^2.1.397",
212
+ "@djangocfg/i18n": "^2.1.400",
213
+ "@djangocfg/typescript-config": "^2.1.400",
214
+ "@djangocfg/ui-core": "^2.1.400",
215
215
  "@types/lodash-es": "^4.17.12",
216
216
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
217
217
  "@types/node": "^24.7.2",
@@ -80,6 +80,26 @@ export interface ChatRootProps {
80
80
  renderHeader?: (ctx: ChatContextValue) => ReactNode;
81
81
  /** Render the empty-state lazily — receives a `setValue` to seed the composer. */
82
82
  renderEmpty?: (api: { setValue: (v: string) => void; focus: () => void }) => ReactNode;
83
+ /**
84
+ * Replace the default `<Composer>` entirely. Receives the live
85
+ * `useChatComposer` return and the composer-related slot props
86
+ * (placeholder, size, attach button, toolbar slots) — host can wire
87
+ * them into a custom widget (e.g. a MarkdownEditor with mentions).
88
+ *
89
+ * When set, `composerToolbarStart/End`, `composerAttachmentTray`,
90
+ * `composerSize`, `showAttachmentButton`, `onPickFiles` are passed
91
+ * to the render-prop but are no longer auto-rendered.
92
+ */
93
+ renderComposer?: (api: {
94
+ composer: UseChatComposerReturn;
95
+ placeholder?: string;
96
+ size?: ComposerSize;
97
+ showAttachmentButton?: boolean;
98
+ onPickFiles?: () => void;
99
+ toolbarStart?: ReactNode;
100
+ toolbarEnd?: ReactNode;
101
+ attachmentTray?: ReactNode;
102
+ }) => ReactNode;
83
103
  /** Forwarded into `<MessageBubble toolCallsProps>` so hosts can swap payload renderers. */
84
104
  toolCallsProps?: Omit<ToolCallsProps, 'calls'>;
85
105
  /** Per-type attachment renderers — `{ image, audio, video, file, default }`. */
@@ -246,16 +266,29 @@ function ChatRootShell({ className, listClassName, slots }: ChatRootShellProps)
246
266
  </div>
247
267
  </div>
248
268
  {!slots.hideComposer && (
249
- <Composer
250
- composer={composer}
251
- placeholder={chat.config.placeholder}
252
- showAttachmentButton={slots.showAttachmentButton}
253
- onPickFiles={slots.onPickFiles}
254
- toolbarStart={slots.composerToolbarStart}
255
- toolbarEnd={slots.composerToolbarEnd}
256
- attachmentTray={slots.composerAttachmentTray}
257
- size={slots.composerSize}
258
- />
269
+ slots.renderComposer
270
+ ? slots.renderComposer({
271
+ composer,
272
+ placeholder: chat.config.placeholder,
273
+ size: slots.composerSize,
274
+ showAttachmentButton: slots.showAttachmentButton,
275
+ onPickFiles: slots.onPickFiles,
276
+ toolbarStart: slots.composerToolbarStart,
277
+ toolbarEnd: slots.composerToolbarEnd,
278
+ attachmentTray: slots.composerAttachmentTray,
279
+ })
280
+ : (
281
+ <Composer
282
+ composer={composer}
283
+ placeholder={chat.config.placeholder}
284
+ showAttachmentButton={slots.showAttachmentButton}
285
+ onPickFiles={slots.onPickFiles}
286
+ toolbarStart={slots.composerToolbarStart}
287
+ toolbarEnd={slots.composerToolbarEnd}
288
+ attachmentTray={slots.composerAttachmentTray}
289
+ size={slots.composerSize}
290
+ />
291
+ )
259
292
  )}
260
293
  {slots.footer ?? null}
261
294
  </div>
@@ -8,7 +8,7 @@ export {
8
8
  } from './MessageList';
9
9
  export { MessageBubble, type MessageBubbleProps } from './MessageBubble';
10
10
  export { MessageActions, type MessageActionsProps } from './MessageActions';
11
- export { Composer, type ComposerProps } from './Composer';
11
+ export { Composer, type ComposerProps, type ComposerSize } from './Composer';
12
12
  export { Sources, type SourcesProps } from './Sources';
13
13
  export { ToolCalls, type ToolCallsProps, type ToolPayloadKind } from './ToolCalls';
14
14
  export {
@@ -201,7 +201,7 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
201
201
  if (!items.length) return;
202
202
  const next = historyRef.current.index < 0 ? items.length - 1 : Math.max(0, historyRef.current.index - 1);
203
203
  historyRef.current.index = next;
204
- setValueState(items[next]);
204
+ setValueState(items[next] ?? '');
205
205
  }, []);
206
206
 
207
207
  const recallNext = useCallback(() => {
@@ -214,7 +214,7 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
214
214
  return;
215
215
  }
216
216
  historyRef.current.index = next;
217
- setValueState(items[next]);
217
+ setValueState(items[next] ?? '');
218
218
  }, []);
219
219
 
220
220
  const onChange = useCallback(
@@ -249,6 +249,7 @@ export {
249
249
  type MessageBubbleProps,
250
250
  type MessageActionsProps,
251
251
  type ComposerProps,
252
+ type ComposerSize,
252
253
  type SourcesProps,
253
254
  type ToolCallsProps,
254
255
  type ToolPayloadKind,
@@ -3,6 +3,12 @@
3
3
  import React, { memo } from 'react';
4
4
  import { CommonExternalProps, JSONTree } from 'react-json-tree';
5
5
 
6
+ // JSON inspector uses a fixed dark palette regardless of the host
7
+ // theme — same convention as PrettyCode and devtool-style data
8
+ // renderers (Chrome DevTools, Insomnia, Bruno). Syntax highlighting
9
+ // has its own contrast model; mixing it with semantic UI tokens on
10
+ // a light surface flattens keys/values into low-contrast pastels.
11
+ const JSON_TREE_SURFACE_BG = '#0d1117';
6
12
  const JSON_TREE_THEME = {
7
13
  scheme: 'djangocfg-dark',
8
14
  base00: 'transparent',
@@ -62,11 +68,11 @@ const JsonContent = memo(({
62
68
  ? (nodeType: string, nodeData: unknown) => {
63
69
  if (nodeType === 'Array') {
64
70
  const length = Array.isArray(nodeData) ? nodeData.length : 0;
65
- return length > 0 ? <span className="text-muted-foreground text-xs">({length})</span> : null;
71
+ return length > 0 ? <span className="text-xs" style={{ color: '#9ca3af' }}>({length})</span> : null;
66
72
  }
67
73
  if (nodeType === 'Object') {
68
74
  const keys = nodeData && typeof nodeData === 'object' ? Object.keys(nodeData) : [];
69
- return keys.length > 0 ? <span className="text-muted-foreground text-xs">{`{${keys.length}}`}</span> : null;
75
+ return keys.length > 0 ? <span className="text-xs" style={{ color: '#9ca3af' }}>{`{${keys.length}}`}</span> : null;
70
76
  }
71
77
  return null;
72
78
  }
@@ -83,9 +89,16 @@ const JsonContent = memo(({
83
89
  typeof value === 'string' && (value.startsWith('http://') || value.startsWith('https://'));
84
90
 
85
91
  return (
86
- <div className={`overflow-auto h-full ${padding}`} style={fontSize ? { fontSize } : undefined}>
92
+ <div
93
+ className={`overflow-auto h-full rounded-md ${padding}`}
94
+ style={{
95
+ backgroundColor: JSON_TREE_SURFACE_BG,
96
+ color: '#e5e7eb',
97
+ ...(fontSize ? { fontSize } : null),
98
+ }}
99
+ >
87
100
  {showTitle && title && (
88
- <h6 className="text-sm font-semibold text-foreground mb-2">{title}</h6>
101
+ <h6 className="text-sm font-semibold mb-2" style={{ color: '#f3f4f6' }}>{title}</h6>
89
102
  )}
90
103
  <JSONTree
91
104
  key={renderKey}
@@ -2,6 +2,20 @@
2
2
  outline: none;
3
3
  }
4
4
 
5
+ /* Inherit semantic foreground so the editor renders correctly in both
6
+ light + dark themes (and under any active preset). Without this the
7
+ browser falls back to UA black on a token-driven background. */
8
+ .markdown-editor .tiptap,
9
+ .markdown-editor .ProseMirror {
10
+ color: var(--color-foreground, var(--foreground));
11
+ }
12
+
13
+ .markdown-editor .tiptap a,
14
+ .markdown-editor .ProseMirror a {
15
+ color: var(--color-primary, var(--primary));
16
+ text-decoration: underline;
17
+ }
18
+
5
19
  .markdown-editor .tiptap h1 {
6
20
  font-size: 1.5em;
7
21
  font-weight: 700;
@@ -46,7 +60,7 @@
46
60
  }
47
61
 
48
62
  .markdown-editor .tiptap blockquote {
49
- border-left: 3px solid var(--border, #333);
63
+ border-left: 3px solid var(--color-border, var(--border));
50
64
  padding-left: 1em;
51
65
  margin: 0.5em 0;
52
66
  opacity: 0.8;
@@ -54,12 +68,13 @@
54
68
 
55
69
  .markdown-editor .tiptap hr {
56
70
  border: none;
57
- border-top: 1px solid var(--border, #333);
71
+ border-top: 1px solid var(--color-border, var(--border));
58
72
  margin: 0.75em 0;
59
73
  }
60
74
 
61
75
  .markdown-editor .tiptap code {
62
- background: var(--muted, #222);
76
+ background: var(--color-muted, var(--muted));
77
+ color: var(--color-muted-foreground, var(--muted-foreground));
63
78
  padding: 0.15em 0.3em;
64
79
  border-radius: 0.25em;
65
80
  font-size: 0.9em;
@@ -182,10 +197,10 @@
182
197
 
183
198
  .markdown-toolbar-btn:hover {
184
199
  opacity: 1;
185
- background: var(--muted, #222);
200
+ background: var(--color-muted, var(--muted));
186
201
  }
187
202
 
188
203
  .markdown-toolbar-btn.active {
189
204
  opacity: 1;
190
- background: var(--muted, #222);
205
+ background: var(--color-muted, var(--muted));
191
206
  }
@@ -42,6 +42,7 @@ const isVerticalDiagram = (svgElement: SVGSVGElement): boolean => {
42
42
  const viewBox = svgElement.getAttribute('viewBox');
43
43
  if (viewBox) {
44
44
  const [, , width, height] = viewBox.split(' ').map(Number);
45
+ if (width === undefined || height === undefined) return false;
45
46
  return height > width * 1.5;
46
47
  }
47
48
  const bbox = svgElement.getBBox?.();
@@ -15,7 +15,7 @@ export function useMermaidValidation() {
15
15
  if (lines.length < 2) return false; // Need at least diagram type + one element
16
16
 
17
17
  // Check for common incomplete patterns
18
- const lastLine = lines[lines.length - 1].trim();
18
+ const lastLine = (lines[lines.length - 1] ?? '').trim();
19
19
 
20
20
  // Incomplete if last line ends with arrow without destination
21
21
  if (lastLine.match(/-->?\s*$/)) return false;
@@ -23,6 +23,7 @@ export const isVerticalDiagram = (svgElement: SVGSVGElement): boolean => {
23
23
  const viewBox = svgElement.getAttribute('viewBox');
24
24
  if (viewBox) {
25
25
  const [, , width, height] = viewBox.split(' ').map(Number);
26
+ if (width === undefined || height === undefined) return false;
26
27
  return height > width * 1.5;
27
28
  }
28
29
  const bbox = svgElement.getBBox?.();
@@ -4,10 +4,19 @@ import { Highlight, Language, themes } from 'prism-react-renderer';
4
4
  import React, { useMemo, useRef } from 'react';
5
5
 
6
6
  import { useAppT } from '@djangocfg/i18n';
7
- import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
8
7
  import { FloatingToolbar } from '../../components/FloatingToolbar';
9
8
  import { CopyAction } from '../../components/FloatingToolbar/actions';
10
9
 
10
+ // Surface palette — fixed dark regardless of host theme. Code blocks
11
+ // follow the IDE / GitHub / ChatGPT convention: syntax highlighting
12
+ // ships its own contrast model, and mixing it with light UI surfaces
13
+ // produces low-contrast pastel renders that hurt readability. The
14
+ // surface is hard-coded (not a semantic token) so the palette stays
15
+ // stable when the user flips themes.
16
+ const CODE_SURFACE_BG = '#0d1117';
17
+ const CODE_SURFACE_BORDER = '#1f2937';
18
+ const CODE_INLINE_BG = '#1f2937';
19
+
11
20
  // Load extra Prism grammars (``bash``, ``ruby``, ``java``, ``php``)
12
21
  // that aren't in ``prism-react-renderer``'s default bundle. The hook
13
22
  // below subscribes to its ready state and re-renders this component
@@ -19,6 +28,13 @@ interface PrettyCodeProps {
19
28
  data: string | object;
20
29
  language: Language;
21
30
  className?: string;
31
+ /**
32
+ * Force a specific Prism palette. **Default is always `dark`** —
33
+ * code blocks ship their own contrast model and we render them on
34
+ * a fixed dark surface regardless of the surrounding UI theme
35
+ * (GitHub / VSCode / ChatGPT convention). Override only if you
36
+ * have a deliberate reason — e.g. printing a PDF on a light page.
37
+ */
22
38
  mode?: 'dark' | 'light';
23
39
  inline?: boolean;
24
40
  customBg?: string; // Custom background class
@@ -38,10 +54,9 @@ interface PrettyCodeProps {
38
54
  variant?: 'card' | 'plain';
39
55
  }
40
56
 
41
- const PrettyCode = ({ data, language, className, mode, inline = false, customBg, isCompact = false, scrollIsolation, maxLines, variant = 'card' }: PrettyCodeProps) => {
57
+ const PrettyCode = ({ data, language, className, mode = 'dark', inline = false, customBg, isCompact = false, scrollIsolation, maxLines, variant = 'card' }: PrettyCodeProps) => {
42
58
  const containerRef = useRef<HTMLDivElement>(null);
43
59
  const t = useAppT();
44
- const detectedTheme = useResolvedTheme();
45
60
 
46
61
  // Subscribe to the extra-grammars ready state. When ``bash`` /
47
62
  // ``ruby`` / ``java`` / ``php`` finish loading, this hook triggers a
@@ -56,12 +71,12 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
56
71
  // Font size based on compact mode
57
72
  const fontSize = isCompact ? '0.75rem' : '0.875rem'; // 12px vs 14px
58
73
 
59
- // Use provided mode or fall back to detected theme
60
- const currentTheme = mode || detectedTheme;
61
- const isDarkMode = currentTheme === 'dark';
62
-
63
- // Select the Prism theme based on the current theme
64
- const prismTheme = isDarkMode ? themes.vsDark : themes.vsLight;
74
+ const isDarkMode = mode !== 'light';
75
+ const prismTheme = isDarkMode ? themes.vsDark : themes.github;
76
+ const surfaceStyle = isDarkMode
77
+ ? { backgroundColor: CODE_SURFACE_BG, borderColor: CODE_SURFACE_BORDER }
78
+ : undefined;
79
+ const inlineSurfaceStyle = isDarkMode ? { backgroundColor: CODE_INLINE_BG } : undefined;
65
80
 
66
81
  // Convert form object to JSON string with proper formatting
67
82
  const contentJson = typeof data === 'string' ? data : JSON.stringify(data || {}, null, 2);
@@ -81,12 +96,16 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
81
96
 
82
97
  // Handle empty content
83
98
  if (!contentJson || contentJson.trim() === '') {
84
- const emptyBgClass = customBg || (isDarkMode ? 'bg-zinc-900' : 'bg-white');
85
- const emptyBorderClass = isDarkMode ? 'border-zinc-700' : 'border-border';
99
+ const emptyBgClass = customBg || (isDarkMode ? '' : 'bg-card');
86
100
  return (
87
- <div className={`relative h-full ${emptyBgClass} rounded-sm border ${emptyBorderClass} ${className || ''}`}>
101
+ <div
102
+ className={`relative h-full ${emptyBgClass} rounded-sm border ${isDarkMode ? '' : 'border-border'} ${className || ''}`}
103
+ style={customBg ? undefined : surfaceStyle}
104
+ >
88
105
  <div className="h-full overflow-auto p-4">
89
- <p className="text-muted-foreground text-sm italic">{labels.noContent}</p>
106
+ <p className="text-sm italic" style={{ color: isDarkMode ? '#9ca3af' : undefined }}>
107
+ {labels.noContent}
108
+ </p>
90
109
  </div>
91
110
  </div>
92
111
  );
@@ -248,7 +267,7 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
248
267
  }
249
268
 
250
269
  if (inline) {
251
- const inlineBgClass = customBg || (isDarkMode ? 'bg-zinc-800' : 'bg-zinc-100');
270
+ const inlineBgClass = customBg || (isDarkMode ? '' : 'bg-muted');
252
271
  return (
253
272
  <Highlight theme={prismTheme} code={contentJson} language={normalizedLanguage as Language}>
254
273
  {({ className, style, tokens, getTokenProps }) => (
@@ -256,6 +275,7 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
256
275
  className={`${className} ${inlineBgClass} px-2 py-1 rounded ${isCompact ? 'text-xs' : 'text-sm'} font-mono inline-block`}
257
276
  style={{
258
277
  ...style,
278
+ ...(customBg ? undefined : inlineSurfaceStyle),
259
279
  fontSize,
260
280
  fontFamily: 'monospace',
261
281
  }}
@@ -271,19 +291,20 @@ const PrettyCode = ({ data, language, className, mode, inline = false, customBg,
271
291
  );
272
292
  }
273
293
 
274
- // Different backgrounds for dark/light - light mode uses white for better contrast with vsLight theme
275
- const bgClass = customBg || (isDarkMode ? 'bg-zinc-900' : 'bg-white');
276
- const borderClass = isDarkMode ? 'border-zinc-700' : 'border-border';
294
+ // Code surface is fixed (dark by default). Falls back to semantic
295
+ // `bg-card` only when the caller explicitly opts into light mode.
296
+ const bgClass = customBg || (isDarkMode ? '' : 'bg-card');
277
297
 
278
298
  return (
279
299
  <div
280
300
  ref={containerRef}
281
- className={`group relative ${bgClass} rounded-lg border ${borderClass} ${className || ''}`}
282
- style={
301
+ className={`group relative ${bgClass} rounded-lg border ${isDarkMode ? '' : 'border-border'} ${className || ''}`}
302
+ style={{
303
+ ...(customBg ? undefined : surfaceStyle),
283
304
  // maxHeight caps growth at ``maxLines`` rows; without maxLines we
284
305
  // let the block grow to fit its content (no scroll).
285
- maxHeightPx ? { maxHeight: `${maxHeightPx}px` } : undefined
286
- }
306
+ ...(maxHeightPx ? { maxHeight: `${maxHeightPx}px` } : null),
307
+ }}
287
308
  >
288
309
  {/* Toolbar: hidden by default, appears on hover. Absolute overlay so it doesn't shift layout.
289
310
  scrollIsolation is force-disabled when content fits without scrolling —
@@ -143,7 +143,9 @@ export function createWebSpeechEngine(
143
143
  rec.onresult = (e) => {
144
144
  for (let i = e.resultIndex; i < e.results.length; i += 1) {
145
145
  const res = e.results[i];
146
+ if (!res) continue;
146
147
  const alt = res[0];
148
+ if (!alt) continue;
147
149
  const text = alt.transcript;
148
150
  if (!currentSegmentId) currentSegmentId = newSegmentId();
149
151
  if (res.isFinal) {