@djangocfg/ui-tools 2.1.349 → 2.1.351

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.349",
3
+ "version": "2.1.351",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -101,8 +101,8 @@
101
101
  "check": "tsc --noEmit"
102
102
  },
103
103
  "peerDependencies": {
104
- "@djangocfg/i18n": "^2.1.349",
105
- "@djangocfg/ui-core": "^2.1.349",
104
+ "@djangocfg/i18n": "^2.1.351",
105
+ "@djangocfg/ui-core": "^2.1.351",
106
106
  "consola": "^3.4.2",
107
107
  "lodash-es": "^4.18.1",
108
108
  "lucide-react": "^0.545.0",
@@ -155,10 +155,10 @@
155
155
  "material-file-icons": "^2.4.0"
156
156
  },
157
157
  "devDependencies": {
158
- "@djangocfg/i18n": "^2.1.349",
158
+ "@djangocfg/i18n": "^2.1.351",
159
159
  "@djangocfg/playground": "workspace:*",
160
- "@djangocfg/typescript-config": "^2.1.349",
161
- "@djangocfg/ui-core": "^2.1.349",
160
+ "@djangocfg/typescript-config": "^2.1.351",
161
+ "@djangocfg/ui-core": "^2.1.351",
162
162
  "@types/lodash-es": "^4.17.12",
163
163
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
164
164
  "@types/node": "^24.7.2",
@@ -101,6 +101,39 @@ export const Default = () => {
101
101
  );
102
102
  };
103
103
 
104
+ // ---------------------------------------------------------------------------
105
+ // 1b) ComposerSizes — sm / md / lg side-by-side
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export const ComposerSizes = () => {
109
+ const make = () =>
110
+ createMockTransport({
111
+ replies: ['I scale with the composer size — try `lg` on a primary surface.'],
112
+ latencyMs: 25,
113
+ });
114
+ const cells: Array<{ size: 'sm' | 'md' | 'lg'; label: string }> = [
115
+ { size: 'sm', label: 'sm — 32px (compact)' },
116
+ { size: 'md', label: 'md — 36px (default)' },
117
+ { size: 'lg', label: 'lg — 48px (primary surface)' },
118
+ ];
119
+ return (
120
+ <div className="grid h-[640px] grid-cols-3 gap-3">
121
+ {cells.map(({ size, label }) => (
122
+ <Frame key={size} h={620}>
123
+ <ChatRoot
124
+ transport={make()}
125
+ config={{
126
+ greeting: label,
127
+ placeholder: 'Type a message…',
128
+ }}
129
+ composerSize={size}
130
+ />
131
+ </Frame>
132
+ ))}
133
+ </div>
134
+ );
135
+ };
136
+
104
137
  // ---------------------------------------------------------------------------
105
138
  // 2) WithToolCalls — scripted tool invocations
106
139
  // ---------------------------------------------------------------------------
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Mute / unmute toggle for chat audio events.
5
+ *
6
+ * Reads the current ``muted`` state from the active chat context and
7
+ * persists changes through ``useChatAudioPrefs`` (cross-tab safe). The
8
+ * button auto-hides when no ``audio.sounds`` config is provided —
9
+ * showing a mute toggle for a chat with no sounds is just clutter.
10
+ *
11
+ * Drop into a ChatRoot ``header`` slot or anywhere inside a chat
12
+ * provider:
13
+ *
14
+ * ```tsx
15
+ * <ChatRoot
16
+ * header={<AudioToggle />}
17
+ * audio={{ sounds: { messageReceived: '/ping.mp3' } }}
18
+ * ...
19
+ * />
20
+ * ```
21
+ */
22
+
23
+ import { Volume2, VolumeX } from 'lucide-react';
24
+
25
+ import { Button } from '@djangocfg/ui-core/components';
26
+ import { cn } from '@djangocfg/ui-core/lib';
27
+
28
+ import { useChatContextOptional } from '../context';
29
+ import { useChatAudioPrefs } from '../core/audio/preferences';
30
+
31
+ export interface AudioToggleProps {
32
+ /** Visual size — matches Button sizes. Default: ``icon``. */
33
+ size?: 'sm' | 'icon';
34
+ /** Variant passed to the underlying Button. Default: ``ghost``. */
35
+ variant?: 'ghost' | 'outline' | 'secondary';
36
+ /** Force-show even when no audio config is wired (e.g. for stories). */
37
+ alwaysShow?: boolean;
38
+ className?: string;
39
+ }
40
+
41
+ export function AudioToggle({
42
+ size = 'icon',
43
+ variant = 'ghost',
44
+ alwaysShow = false,
45
+ className,
46
+ }: AudioToggleProps) {
47
+ // Read straight from the persist store so the toggle works even
48
+ // when rendered OUTSIDE the ChatRoot (e.g. in a parent header). The
49
+ // chat audio bus reads the same store, so a click here flips the
50
+ // "muted" state for any sibling ChatRoot in the same tab and
51
+ // mirrors across tabs via the storage event.
52
+ const muted = useChatAudioPrefs((s) => s.muted);
53
+ const setMuted = useChatAudioPrefs((s) => s.setMuted);
54
+
55
+ // If a ChatRoot is in scope, hide unless it actually wired sounds —
56
+ // otherwise the button is a no-op for that surface. When rendered
57
+ // standalone (no context), default to visible.
58
+ const ctx = useChatContextOptional();
59
+ if (ctx && !ctx.hasAudio && !alwaysShow) return null;
60
+
61
+ const Icon = muted ? VolumeX : Volume2;
62
+ const label = muted ? 'Unmute chat sounds' : 'Mute chat sounds';
63
+
64
+ return (
65
+ <Button
66
+ type="button"
67
+ variant={variant}
68
+ size={size}
69
+ onClick={() => setMuted(!muted)}
70
+ aria-label={label}
71
+ aria-pressed={muted}
72
+ title={label}
73
+ className={cn(size === 'icon' ? 'h-9 w-9' : '', className)}
74
+ >
75
+ <Icon aria-hidden className="size-4" />
76
+ </Button>
77
+ );
78
+ }
@@ -10,7 +10,7 @@ import { ChatProvider, useChatContext, type ChatContextValue } from '../context'
10
10
  import { useChatComposer, type UseChatComposerReturn } from '../hooks/useChatComposer';
11
11
  import { useChatScroll } from '../hooks/useChatScroll';
12
12
  import { useChatHistory } from '../hooks/useChatHistory';
13
- import { Composer } from './Composer';
13
+ import { Composer, type ComposerSize } from './Composer';
14
14
  import { EmptyState } from './EmptyState';
15
15
  import { ErrorBanner } from './ErrorBanner';
16
16
  import { JumpToLatest } from './JumpToLatest';
@@ -75,6 +75,9 @@ export interface ChatRootProps {
75
75
  onPickFiles?: () => void;
76
76
  /** Hide the composer input area entirely (e.g. while waiting for human approval). */
77
77
  hideComposer?: boolean;
78
+ /** Composer size variant. Default ``md`` (36px slot). Use ``lg`` for primary
79
+ * surfaces (onboarding, full-page chat), ``sm`` for dense sidebars. */
80
+ composerSize?: ComposerSize;
78
81
  }
79
82
 
80
83
  export function ChatRoot(props: ChatRootProps) {
@@ -195,6 +198,7 @@ function ChatRootShell({ className, slots }: ChatRootShellProps) {
195
198
  toolbarStart={slots.composerToolbarStart}
196
199
  toolbarEnd={slots.composerToolbarEnd}
197
200
  attachmentTray={slots.composerAttachmentTray}
201
+ size={slots.composerSize}
198
202
  />
199
203
  )}
200
204
  {slots.footer ?? null}
@@ -10,6 +10,8 @@ import { useChatContextOptional } from '../context';
10
10
  import type { UseChatComposerReturn } from '../hooks/useChatComposer';
11
11
  import { Attachments } from './Attachments';
12
12
 
13
+ export type ComposerSize = 'sm' | 'md' | 'lg';
14
+
13
15
  export interface ComposerProps {
14
16
  composer: UseChatComposerReturn;
15
17
  placeholder?: string;
@@ -21,11 +23,57 @@ export interface ComposerProps {
21
23
  attachmentTray?: ReactNode;
22
24
  className?: string;
23
25
  textareaClassName?: string;
26
+ /** Visual size — controls textarea height + button slot size.
27
+ *
28
+ * - ``sm`` — 32px slot, dense compact composer (admin sidebars, etc).
29
+ * - ``md`` — 36px slot, default. Same as the legacy fixed size.
30
+ * - ``lg`` — 48px slot, generous textarea. Use when the chat is
31
+ * the page's primary surface (onboarding, support).
32
+ */
33
+ size?: ComposerSize;
24
34
  /** Show "Stop" button instead of "Send" while streaming. */
25
35
  isStreaming?: boolean;
26
36
  onCancel?: () => void;
27
37
  }
28
38
 
39
+ const SIZE_CLASSES: Record<ComposerSize, {
40
+ slot: string;
41
+ button: string;
42
+ iconButton: string;
43
+ textarea: string;
44
+ text: string;
45
+ padding: string;
46
+ containerPadding: string;
47
+ }> = {
48
+ sm: {
49
+ slot: '[&>:not(textarea)]:h-8',
50
+ button: 'h-8 w-8',
51
+ iconButton: 'size-3.5',
52
+ textarea: 'min-h-8 max-h-48 px-3 py-1.5',
53
+ text: 'text-sm',
54
+ padding: 'gap-1.5',
55
+ containerPadding: 'px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))]',
56
+ },
57
+ md: {
58
+ slot: '[&>:not(textarea)]:h-9',
59
+ button: 'h-9 w-9',
60
+ iconButton: 'size-4',
61
+ textarea: 'min-h-9 max-h-60 px-3.5 py-2',
62
+ text: 'text-base sm:text-sm',
63
+ padding: 'gap-1.5',
64
+ containerPadding: 'px-2.5 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]',
65
+ },
66
+ lg: {
67
+ slot: '[&>:not(textarea)]:h-12',
68
+ button: 'h-12 w-12',
69
+ iconButton: 'size-5',
70
+ textarea: 'min-h-12 max-h-72 px-4 py-3',
71
+ text: 'text-base',
72
+ padding: 'gap-2',
73
+ containerPadding: 'px-3.5 pt-3 pb-[max(0.875rem,env(safe-area-inset-bottom))]',
74
+ },
75
+ };
76
+
29
77
  export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Composer(
30
78
  {
31
79
  composer,
@@ -38,6 +86,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
38
86
  attachmentTray,
39
87
  className,
40
88
  textareaClassName,
89
+ size = 'md',
41
90
  isStreaming: isStreamingProp,
42
91
  onCancel: onCancelProp,
43
92
  },
@@ -47,12 +96,14 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
47
96
  const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;
48
97
  const onCancel = onCancelProp ?? ctx?.cancelStream;
49
98
  const isDisabled = disabled ?? isStreaming;
99
+ const sz = SIZE_CLASSES[size];
50
100
 
51
101
  return (
52
102
  <div
53
103
  ref={ref}
54
104
  className={cn(
55
- 'border-t border-border bg-background/95 px-2.5 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]',
105
+ 'border-t border-border bg-background/95',
106
+ sz.containerPadding,
56
107
  className,
57
108
  )}
58
109
  >
@@ -67,11 +118,11 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
67
118
  </div>
68
119
  ) : null}
69
120
 
70
- {/* `[&>*]:h-9` enforces a consistent 36px slot height so toolbar
71
- * buttons line up with the textarea baseline (`min-h-[36px]`).
72
- * Toolbar slots that want to opt out can pass an explicit class
73
- * like `!h-auto`. */}
74
- <div className="flex items-end gap-1.5 [&>:not(textarea)]:shrink-0 [&>:not(textarea)]:h-9">
121
+ {/* Size-aware slot row. ``[&>:not(textarea)]:h-{N}`` enforces a
122
+ * consistent slot height so toolbar buttons line up with the
123
+ * textarea baseline. Toolbar slots that want to opt out can
124
+ * pass an explicit class like `!h-auto`. */}
125
+ <div className={cn('flex items-end [&>:not(textarea)]:shrink-0', sz.padding, sz.slot)}>
75
126
  {showAttachmentButton ? (
76
127
  <Button
77
128
  type="button"
@@ -80,9 +131,9 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
80
131
  onClick={onPickFiles}
81
132
  aria-label="Attach files"
82
133
  disabled={isDisabled}
83
- className="h-9 w-9"
134
+ className={sz.button}
84
135
  >
85
- <Paperclip aria-hidden className="size-4" />
136
+ <Paperclip aria-hidden className={sz.iconButton} />
86
137
  </Button>
87
138
  ) : null}
88
139
 
@@ -96,7 +147,9 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
96
147
  aria-multiline="true"
97
148
  disabled={isDisabled}
98
149
  className={cn(
99
- 'min-h-9 max-h-60 flex-1 resize-none rounded-2xl px-3.5 py-2 text-base sm:text-sm',
150
+ 'flex-1 resize-none rounded-2xl',
151
+ sz.textarea,
152
+ sz.text,
100
153
  textareaClassName,
101
154
  )}
102
155
  />
@@ -111,9 +164,9 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
111
164
  onClick={onCancel}
112
165
  aria-label="Stop"
113
166
  aria-keyshortcuts="Escape"
114
- className="h-9 w-9"
167
+ className={sz.button}
115
168
  >
116
- <Square aria-hidden className="size-3.5" />
169
+ <Square aria-hidden className={sz.iconButton} />
117
170
  </Button>
118
171
  ) : (
119
172
  <Button
@@ -123,9 +176,9 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
123
176
  disabled={!composer.canSubmit}
124
177
  aria-label="Send"
125
178
  aria-keyshortcuts="Enter"
126
- className="h-9 w-9"
179
+ className={sz.button}
127
180
  >
128
- <Send aria-hidden className="size-4" />
181
+ <Send aria-hidden className={sz.iconButton} />
129
182
  </Button>
130
183
  )}
131
184
  </div>
@@ -22,3 +22,4 @@ export { EmptyState, type EmptyStateProps } from './EmptyState';
22
22
  export { ErrorBanner, type ErrorBannerProps } from './ErrorBanner';
23
23
  export { JumpToLatest, type JumpToLatestProps } from './JumpToLatest';
24
24
  export { StreamingIndicator, type StreamingIndicatorProps } from './StreamingIndicator';
25
+ export { AudioToggle, type AudioToggleProps } from './AudioToggle';
@@ -22,6 +22,10 @@ export interface ChatContextValue extends UseChatReturn {
22
22
  config: ChatConfig;
23
23
  labels: ChatLabels;
24
24
  audio: UseChatAudioReturn;
25
+ /** True iff the host wired at least one ``audio.sounds[event]`` URL.
26
+ * Components like ``AudioToggle`` use this to auto-hide when there
27
+ * is nothing to mute. */
28
+ hasAudio: boolean;
25
29
  }
26
30
 
27
31
  const Ctx = createContext<ChatContextValue | null>(null);
@@ -101,9 +105,17 @@ export function ChatProvider({
101
105
  [config.labels],
102
106
  );
103
107
 
108
+ const hasAudio = useMemo<boolean>(() => {
109
+ const sounds = audio?.sounds;
110
+ if (!sounds) return false;
111
+ return Object.values(sounds).some(
112
+ (v) => typeof v === 'string' && v.length > 0,
113
+ );
114
+ }, [audio]);
115
+
104
116
  const value = useMemo<ChatContextValue>(
105
- () => ({ ...chat, layout, config, labels, audio: audioApi }),
106
- [chat, layout, config, labels, audioApi],
117
+ () => ({ ...chat, layout, config, labels, audio: audioApi, hasAudio }),
118
+ [chat, layout, config, labels, audioApi, hasAudio],
107
119
  );
108
120
 
109
121
  return (
@@ -137,6 +137,7 @@ export {
137
137
  ErrorBanner,
138
138
  JumpToLatest,
139
139
  StreamingIndicator,
140
+ AudioToggle,
140
141
  type ChatRootProps,
141
142
  type MessageListProps,
142
143
  type MessageBubbleProps,
@@ -155,6 +156,7 @@ export {
155
156
  type ErrorBannerProps,
156
157
  type JumpToLatestProps,
157
158
  type StreamingIndicatorProps,
159
+ type AudioToggleProps,
158
160
  } from './components';
159
161
 
160
162
  // Lazy preset
@@ -1,5 +0,0 @@
1
- export { ChatRoot } from './chunk-QCKJVSJF.mjs';
2
- import './chunk-2ZLKZ5VR.mjs';
3
- import './chunk-N2XQF2OL.mjs';
4
- //# sourceMappingURL=ChatRoot-CZMOJOGD.mjs.map
5
- //# sourceMappingURL=ChatRoot-CZMOJOGD.mjs.map
@@ -1,14 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkXYGB26BY_cjs = require('./chunk-XYGB26BY.cjs');
4
- require('./chunk-B5AWZOHJ.cjs');
5
- require('./chunk-OLISEQHS.cjs');
6
-
7
-
8
-
9
- Object.defineProperty(exports, "ChatRoot", {
10
- enumerable: true,
11
- get: function () { return chunkXYGB26BY_cjs.ChatRoot; }
12
- });
13
- //# sourceMappingURL=ChatRoot-VTYANC7M.cjs.map
14
- //# sourceMappingURL=ChatRoot-VTYANC7M.cjs.map