@sybilion/uilib 1.3.20 → 1.3.22

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,7 +1,7 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import { MessageRole, GENERATING_DASHBOARD_SYSTEM_TEXT } from '../Chat.types.js';
4
- import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant, isPresetScriptGraph, branchesFromPresetScriptGraph } from '../ChatMessage/presetScript.js';
4
+ import { isGraphIntakeAssistantStepComplete, matchUserTextToQuickReply, isPresetScriptGraph, branchesFromPresetScriptGraph, parseScriptLine, textHasQuickReplyMarkers, branchKeysUsedFromChatHistory, branchKeysUsedByUserMessages, extractQuickReplyLabelKeyPairsFromText, entryBranchKeyBeforeLastAssistant } from '../ChatMessage/presetScript.js';
5
5
  import { buildChatSendMessagePayload, displayTextFromSendPayload } from '../buildChatSendMessagePayload.js';
6
6
  import { usedPresetIdsFromMessages, formatChatTranscript } from '../chat-preset-utils.js';
7
7
  import { useChatsForScopeId, useChat, useChatOutboundPending, useSyncChatPanelBusy, isChatEmpty } from '../../../../contexts/chat-context.js';
@@ -425,7 +425,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
425
425
  onMessage,
426
426
  onScriptComplete,
427
427
  ]);
428
- const submitPreset = async (preset) => {
428
+ const submitPreset = useCallback(async (preset) => {
429
429
  const script = preset.script;
430
430
  const scriptGraph = isPresetScriptGraph(script);
431
431
  const hasLinearScript = Array.isArray(script) && script.length > 0;
@@ -518,7 +518,24 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
518
518
  finally {
519
519
  setLocalUiBusy(false);
520
520
  }
521
- };
521
+ }, [
522
+ currentChatId,
523
+ endLocalDemoFlow,
524
+ handlePromptSubmit,
525
+ addMessage,
526
+ presetsWithFreeform,
527
+ ]);
528
+ const resolvedEmptyState = useMemo(() => {
529
+ if (!emptyState)
530
+ return undefined;
531
+ const { additionalContent, ...rest } = emptyState;
532
+ return {
533
+ ...rest,
534
+ additionalContent: typeof additionalContent === 'function'
535
+ ? additionalContent({ submitPreset })
536
+ : additionalContent,
537
+ };
538
+ }, [emptyState, submitPreset]);
522
539
  const activeScript = currentChatId
523
540
  ? scriptByChatId[currentChatId]
524
541
  : undefined;
@@ -765,7 +782,7 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
765
782
  onPromptSubmit: handlePromptSubmit,
766
783
  onChatDeleted: endLocalDemoFlow,
767
784
  promptPrefill: promptLinkPrefill,
768
- emptyState,
785
+ emptyState: resolvedEmptyState,
769
786
  allowedAttachments,
770
787
  allowPdfAttachments,
771
788
  onAttachmentsDropped,
@@ -1,13 +1,15 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import cn from 'classnames';
3
- import React__default, { useState, useRef, useId, useEffect } from 'react';
3
+ import React__default, { useState, useRef, useId, useEffect, useLayoutEffect } from 'react';
4
4
  import { createPortal } from 'react-dom';
5
5
  import { Card, CardHeader, CardContent, CardFooter } from '../Card/Card.js';
6
+ import { APP_MODAL_ID } from '../../../constants/appMount.js';
6
7
  import { XIcon } from '@phosphor-icons/react';
7
8
  import S from './Dialog.styl.js';
8
9
 
9
10
  function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disableCloseButton, icon, content, contentClassName, footer, footerClassName, footerAlignment = 'center', size = 'default', className, noScroll, maxHeight, autoScrollBottom, width, }) {
10
11
  const [isMounted, setIsMounted] = useState(false);
12
+ const [modalContainer, setModalContainer] = useState(null);
11
13
  const [isAnimating, setIsAnimating] = useState(false);
12
14
  const dialogRef = useRef(null);
13
15
  const triggerRef = useRef(null);
@@ -19,6 +21,9 @@ function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disabl
19
21
  setIsMounted(true);
20
22
  return () => setIsMounted(false);
21
23
  }, []);
24
+ useLayoutEffect(() => {
25
+ setModalContainer(document.getElementById(APP_MODAL_ID));
26
+ }, []);
22
27
  // Handle open/close animations
23
28
  useEffect(() => {
24
29
  if (open) {
@@ -120,7 +125,10 @@ function Dialog({ open, onOpenChange, disabled, trigger, title, subtitle, disabl
120
125
  : null,
121
126
  }, children: [(title || subtitle || icon) && (jsx(CardHeader, { icon: icon, title: title, description: subtitle })), !disableCloseButton && (jsx("button", { className: S.dialogClose, onClick: handleCloseClick, "aria-label": "Close dialog", type: "button", children: jsx(XIcon, { size: 16 }) })), content && (jsx(CardContent, { className: contentClassName, noScroll: noScroll, autoScrollBottom: autoScrollBottom, children: content })), footer && (jsx(CardFooter, { className: cn(S.footer, S[`align-${footerAlignment}`], footerClassName), children: footer }))] })] }));
122
127
  const onTriggerClick = !disabled ? () => onOpenChange(true) : undefined;
123
- return (jsxs(Fragment, { children: [trigger && jsx("div", { onClick: onTriggerClick, children: trigger }), open && !disabled && createPortal(dialogContent, document.body)] }));
128
+ return (jsxs(Fragment, { children: [trigger && jsx("div", { onClick: onTriggerClick, children: trigger }), open &&
129
+ !disabled &&
130
+ modalContainer &&
131
+ createPortal(dialogContent, modalContainer)] }));
124
132
  }
125
133
 
126
134
  export { Dialog, S as DialogStyles };
@@ -0,0 +1,5 @@
1
+ /** DOM id for the main React root mount (`#app-root`). */
2
+ /** DOM id for modal/dialog portals (`#app-modal`). */
3
+ const APP_MODAL_ID = 'app-modal';
4
+
5
+ export { APP_MODAL_ID };
@@ -1,7 +1,15 @@
1
+ import type { ChatPreset } from '#uilib/components/ui/Chat/Chat.types';
2
+ export type ChatEmptyStateContext = {
3
+ submitPreset: (preset: ChatPreset) => void | Promise<void>;
4
+ };
1
5
  export interface ChatEmptyStateProps {
2
6
  icon?: React.ReactNode;
3
7
  title?: string;
4
8
  description?: string;
5
- /** Extra block below description (works when passed via `ChatChrome` / `ChatSheet` `emptyState`). */
9
+ /** Extra block below description (resolved before render in `ChatEmptyState`). */
6
10
  additionalContent?: React.ReactNode;
7
11
  }
12
+ /** Passed to `ChatSheet` / `useChatPanelChromeModel`; function form resolved before render. */
13
+ export interface ChatEmptyStateConfig extends Omit<ChatEmptyStateProps, 'additionalContent'> {
14
+ additionalContent?: React.ReactNode | ((ctx: ChatEmptyStateContext) => React.ReactNode);
15
+ }
@@ -1,7 +1,7 @@
1
1
  import { ChatPreset, type ScriptCompletePayload } from '#uilib/components/ui/Chat/Chat.types';
2
2
  import type { ChatChromeProps } from '../ChatChrome';
3
3
  import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
4
- import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
4
+ import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.types';
5
5
  export type UseChatPanelChromeModelInput = {
6
6
  /** When true, skip sidebar chat slot, URL `?chat=`, and portal behavior (e.g. page main content). */
7
7
  embedAsPage: boolean;
@@ -16,7 +16,7 @@ export type UseChatPanelChromeModelInput = {
16
16
  /** Renders `[CHART]` tokens in assistant messages. */
17
17
  renderMessageChart?: () => React.ReactNode;
18
18
  /** Forwarded to `ChatChrome` when the thread is empty. */
19
- emptyState?: ChatEmptyStateProps;
19
+ emptyState?: ChatEmptyStateConfig;
20
20
  /** MIME types / extensions for text-only chat attachments (filtered by uilib allowlist). */
21
21
  allowedAttachments?: readonly string[];
22
22
  /** When true, PDF drops are accepted and parsed to plain text. */
@@ -12,6 +12,7 @@ export type { UseChatPanelChromeModelInput, UseChatPanelChromeModelResult, } fro
12
12
  export { ChatMessage } from './ChatMessage';
13
13
  export { ChatPrompt } from './ChatPrompt';
14
14
  export { ChatPresets } from './ChatPresets';
15
+ export type { ChatEmptyStateConfig, ChatEmptyStateContext, ChatEmptyStateProps, } from './ChatEmptyState/ChatEmptyState.types';
15
16
  export type { Chat as ChatType, ChatAttachmentDropItem, ChatSendMessagePayload, ChatProps, ChatPreset as ChatPresetType, Message, UserTextFileAttachment, } from './Chat.types';
16
17
  export { MessageRole } from './Chat.types';
17
18
  export { CsvIcon } from '../../icons/CsvIcon/CsvIcon';
@@ -0,0 +1,4 @@
1
+ /** DOM id for the main React root mount (`#app-root`). */
2
+ export declare const APP_ROOT_ID = "app-root";
3
+ /** DOM id for modal/dialog portals (`#app-modal`). */
4
+ export declare const APP_MODAL_ID = "app-modal";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.20",
3
+ "version": "1.3.22",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,7 +1,23 @@
1
+ import type { ChatPreset } from '#uilib/components/ui/Chat/Chat.types';
2
+
3
+ export type ChatEmptyStateContext = {
4
+ submitPreset: (preset: ChatPreset) => void | Promise<void>;
5
+ };
6
+
1
7
  export interface ChatEmptyStateProps {
2
8
  icon?: React.ReactNode;
3
9
  title?: string;
4
10
  description?: string;
5
- /** Extra block below description (works when passed via `ChatChrome` / `ChatSheet` `emptyState`). */
11
+ /** Extra block below description (resolved before render in `ChatEmptyState`). */
6
12
  additionalContent?: React.ReactNode;
7
13
  }
14
+
15
+ /** Passed to `ChatSheet` / `useChatPanelChromeModel`; function form resolved before render. */
16
+ export interface ChatEmptyStateConfig extends Omit<
17
+ ChatEmptyStateProps,
18
+ 'additionalContent'
19
+ > {
20
+ additionalContent?:
21
+ | React.ReactNode
22
+ | ((ctx: ChatEmptyStateContext) => React.ReactNode);
23
+ }
@@ -45,6 +45,7 @@ import { useSidebar } from '../../Sidebar/Sidebar';
45
45
  import { Chat } from '../Chat';
46
46
  import type { ChatChromeProps } from '../ChatChrome';
47
47
  import type { ChatAttachmentDropItem } from '../ChatChrome/ChatChrome.types';
48
+ import type { ChatEmptyStateConfig } from '../ChatEmptyState/ChatEmptyState.types';
48
49
  import type { ChatEmptyStateProps } from '../ChatEmptyState/ChatEmptyState.types';
49
50
 
50
51
  export type UseChatPanelChromeModelInput = {
@@ -61,7 +62,7 @@ export type UseChatPanelChromeModelInput = {
61
62
  /** Renders `[CHART]` tokens in assistant messages. */
62
63
  renderMessageChart?: () => React.ReactNode;
63
64
  /** Forwarded to `ChatChrome` when the thread is empty. */
64
- emptyState?: ChatEmptyStateProps;
65
+ emptyState?: ChatEmptyStateConfig;
65
66
  /** MIME types / extensions for text-only chat attachments (filtered by uilib allowlist). */
66
67
  allowedAttachments?: readonly string[];
67
68
  /** When true, PDF drops are accepted and parsed to plain text. */
@@ -615,7 +616,7 @@ export function useChatPanelChromeModel({
615
616
  ],
616
617
  );
617
618
 
618
- const submitPreset = async (preset: ChatPreset) => {
619
+ const submitPreset = useCallback(async (preset: ChatPreset) => {
619
620
  const script = preset.script;
620
621
  const scriptGraph = isPresetScriptGraph(script);
621
622
  const hasLinearScript = Array.isArray(script) && script.length > 0;
@@ -705,7 +706,25 @@ export function useChatPanelChromeModel({
705
706
  } finally {
706
707
  setLocalUiBusy(false);
707
708
  }
708
- };
709
+ }, [
710
+ currentChatId,
711
+ endLocalDemoFlow,
712
+ handlePromptSubmit,
713
+ addMessage,
714
+ presetsWithFreeform,
715
+ ]);
716
+
717
+ const resolvedEmptyState = useMemo((): ChatEmptyStateProps | undefined => {
718
+ if (!emptyState) return undefined;
719
+ const { additionalContent, ...rest } = emptyState;
720
+ return {
721
+ ...rest,
722
+ additionalContent:
723
+ typeof additionalContent === 'function'
724
+ ? additionalContent({ submitPreset })
725
+ : additionalContent,
726
+ };
727
+ }, [emptyState, submitPreset]);
709
728
 
710
729
  const activeScript = currentChatId
711
730
  ? scriptByChatId[currentChatId]
@@ -1017,7 +1036,7 @@ export function useChatPanelChromeModel({
1017
1036
  onPromptSubmit: handlePromptSubmit,
1018
1037
  onChatDeleted: endLocalDemoFlow,
1019
1038
  promptPrefill: promptLinkPrefill,
1020
- emptyState,
1039
+ emptyState: resolvedEmptyState,
1021
1040
  allowedAttachments,
1022
1041
  allowPdfAttachments,
1023
1042
  onAttachmentsDropped,
@@ -25,6 +25,11 @@ export type {
25
25
  export { ChatMessage } from './ChatMessage';
26
26
  export { ChatPrompt } from './ChatPrompt';
27
27
  export { ChatPresets } from './ChatPresets';
28
+ export type {
29
+ ChatEmptyStateConfig,
30
+ ChatEmptyStateContext,
31
+ ChatEmptyStateProps,
32
+ } from './ChatEmptyState/ChatEmptyState.types';
28
33
  export type {
29
34
  Chat as ChatType,
30
35
  ChatAttachmentDropItem,
@@ -1,5 +1,11 @@
1
1
  import cn from 'classnames';
2
- import React, { useEffect, useId, useRef, useState } from 'react';
2
+ import React, {
3
+ useEffect,
4
+ useId,
5
+ useLayoutEffect,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
3
9
  import { createPortal } from 'react-dom';
4
10
 
5
11
  import {
@@ -8,6 +14,7 @@ import {
8
14
  CardFooter,
9
15
  CardHeader,
10
16
  } from '#uilib/components/ui/Card/Card';
17
+ import { APP_MODAL_ID } from '#uilib/constants/appMount';
11
18
  import { XIcon } from '@phosphor-icons/react';
12
19
 
13
20
  import S from './Dialog.styl';
@@ -35,6 +42,9 @@ export function Dialog({
35
42
  width,
36
43
  }: DialogProps) {
37
44
  const [isMounted, setIsMounted] = useState(false);
45
+ const [modalContainer, setModalContainer] = useState<HTMLElement | null>(
46
+ null,
47
+ );
38
48
  const [isAnimating, setIsAnimating] = useState(false);
39
49
  const dialogRef = useRef<HTMLDivElement>(null);
40
50
  const triggerRef = useRef<HTMLElement | null>(null);
@@ -48,6 +58,10 @@ export function Dialog({
48
58
  return () => setIsMounted(false);
49
59
  }, []);
50
60
 
61
+ useLayoutEffect(() => {
62
+ setModalContainer(document.getElementById(APP_MODAL_ID));
63
+ }, []);
64
+
51
65
  // Handle open/close animations
52
66
  useEffect(() => {
53
67
  if (open) {
@@ -228,7 +242,10 @@ export function Dialog({
228
242
  return (
229
243
  <>
230
244
  {trigger && <div onClick={onTriggerClick}>{trigger}</div>}
231
- {open && !disabled && createPortal(dialogContent, document.body)}
245
+ {open &&
246
+ !disabled &&
247
+ modalContainer &&
248
+ createPortal(dialogContent, modalContainer)}
232
249
  </>
233
250
  );
234
251
  }
@@ -0,0 +1,5 @@
1
+ /** DOM id for the main React root mount (`#app-root`). */
2
+ export const APP_ROOT_ID = 'app-root';
3
+
4
+ /** DOM id for modal/dialog portals (`#app-modal`). */
5
+ export const APP_MODAL_ID = 'app-modal';
@@ -1,12 +1,13 @@
1
1
  import { createRoot } from 'react-dom/client';
2
2
  import { BrowserRouter } from 'react-router-dom';
3
3
 
4
+ import { APP_ROOT_ID } from '#uilib/constants/appMount';
4
5
  import { DEFAULT_THEME_ACTIVE_COLOR } from '#uilib/docs/lib/theme';
5
6
 
6
7
  import App from './App/App';
7
8
  import { ThemeProvider } from './contexts/theme-context';
8
9
 
9
- const elem = document.getElementById('app-root') as HTMLElement;
10
+ const elem = document.getElementById(APP_ROOT_ID) as HTMLElement;
10
11
  const root = createRoot(elem);
11
12
 
12
13
  root.render(