@sybilion/uilib 1.3.43 → 1.3.44

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.
@@ -9,7 +9,7 @@ import { useChatPromptEditor } from './useChatPromptEditor.js';
9
9
  function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage, slashCommandItems, onSlashItemCommand, attachments = [], onRemoveAttachment, disabled = false, attachmentAccept, onAttachmentFiles, }) {
10
10
  const attachmentsCount = attachments.length;
11
11
  const emitSubmitRef = useRef(() => { });
12
- const { editor, trimmedMessage, resetAfterSend, handleComposerKeyDown } = useChatPromptEditor({
12
+ const { editor, trimmedMessage, resetAfterSend } = useChatPromptEditor({
13
13
  disabled,
14
14
  placeholder,
15
15
  slashCommandItems,
@@ -42,7 +42,7 @@ function ChatPrompt({ onSubmit, placeholder, className, footer, prefillMessage,
42
42
  if (!editor) {
43
43
  return null;
44
44
  }
45
- return (jsxs("form", { onSubmit: handleSubmitForm, className: cn(S.root, className), children: [jsx(ChatPromptAttachments, { attachments: attachments, onRemove: index => onRemoveAttachment?.(index), disabled: disabled }), jsx(ChatPromptComposer, { editor: editor, disabled: disabled, trimmedMessage: trimmedMessage, attachments: attachments, attachmentAccept: attachmentAccept, onAttachmentFiles: onAttachmentFiles, onComposerKeyDown: handleComposerKeyDown }), footer] }));
45
+ return (jsxs("form", { onSubmit: handleSubmitForm, className: cn(S.root, className), children: [jsx(ChatPromptAttachments, { attachments: attachments, onRemove: index => onRemoveAttachment?.(index), disabled: disabled }), jsx(ChatPromptComposer, { editor: editor, disabled: disabled, trimmedMessage: trimmedMessage, attachments: attachments, attachmentAccept: attachmentAccept, onAttachmentFiles: onAttachmentFiles }), footer] }));
46
46
  }
47
47
 
48
48
  export { ChatPrompt };
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChatPrompt_root__5G5bq{align-items:stretch;display:flex;flex-direction:column;gap:var(--p-2);padding:var(--p-6);padding-top:var(--p-5);position:relative}.ChatPrompt_composer__H3c3N{align-items:flex-end;display:flex;flex-direction:row;gap:var(--p-3);position:relative;width:100%}.ChatPrompt_fileInput__xdgPn{display:none}.ChatPrompt_attachButton__gi-qF{align-self:flex-end;flex-shrink:0}.ChatPrompt_editorWrap__Q7gat{align-self:stretch;flex:1;min-width:0}.ChatPrompt_editorMount__Phh4D{background:transparent;border:none;border-radius:0!important;box-shadow:none!important;display:flex;flex:1;flex-direction:column;max-height:200px;min-height:40px;min-width:0;padding:0!important}.ChatPrompt_editorMount__Phh4D:focus-within{box-shadow:none!important}.ChatPrompt_editorMount__Phh4D .ProseMirror{border:none!important;box-shadow:none!important;flex:1;margin:0;max-height:200px!important;min-height:40px!important;outline:none!important;overflow-x:hidden!important;overflow-y:auto!important;padding:var(--p-2) 0 0!important;resize:none!important;white-space:pre-wrap;word-break:break-word}.ChatPrompt_editorMount__Phh4D .ProseMirror p.is-empty:before{color:var(--muted-foreground);content:attr(data-placeholder);float:left;height:0;pointer-events:none}.ChatPrompt_submitColumn__0rY1R{align-items:center;display:flex;flex-direction:column;flex-shrink:0;justify-content:flex-end}.ChatPrompt_submitColumn__0rY1R>button:focus{box-shadow:0 0 0 2px var(--brand-color-500)!important}.ChatPrompt_submitColumn__0rY1R>button:first-child{border:none;position:relative;transition:all .2s}.ChatPrompt_submitColumn__0rY1R>button:first-child:focus{transform:scale(1.2)}.ChatPrompt_submitColumn__0rY1R>button:first-child:before{bottom:-100%;content:\"\";left:-100%;position:absolute;right:-100%;top:-100%}.ChatPrompt_attachments__KG-fG{display:flex;flex-wrap:wrap;gap:var(--p-2);margin-bottom:var(--p-2)}.ChatPrompt_attachmentItem__QJk7J{flex:1 1 300px;max-width:300px;min-width:0}";
3
+ var css_248z = "@media (max-width:768px){:root{--page-x-padding:var(--p-6);--page-y-padding:var(--p-6)}}.ChatPrompt_root__5G5bq{align-items:stretch;display:flex;flex-direction:column;gap:var(--p-2);padding:var(--p-6);padding-top:var(--p-5);position:relative}.ChatPrompt_composer__H3c3N{align-items:flex-end;display:flex;flex-direction:row;gap:var(--p-3);position:relative;width:100%}.ChatPrompt_fileInput__xdgPn{display:none}.ChatPrompt_attachButton__gi-qF{align-self:flex-end;flex-shrink:0}.ChatPrompt_editorWrap__Q7gat{align-self:stretch;flex:1;min-width:0}.ChatPrompt_editorMount__Phh4D{background:transparent;border:none;border-radius:0!important;box-shadow:none!important;display:flex;flex:1;flex-direction:column;max-height:200px;min-height:40px;min-width:0;padding:0!important}.ChatPrompt_editorMount__Phh4D:focus-within{box-shadow:none!important}.ChatPrompt_editorMount__Phh4D .ProseMirror{border:none!important;box-shadow:none!important;flex:1;margin:0;max-height:200px!important;min-height:40px!important;outline:none!important;overflow-x:hidden!important;overflow-y:auto!important;padding:var(--p-2) 0 0!important;resize:none!important;white-space:pre-wrap;word-break:break-word}.ChatPrompt_editorMount__Phh4D .ProseMirror p.is-empty.is-editor-empty:before{color:var(--muted-foreground);content:attr(data-placeholder);float:left;height:0;pointer-events:none}.ChatPrompt_submitColumn__0rY1R{align-items:center;display:flex;flex-direction:column;flex-shrink:0;justify-content:flex-end}.ChatPrompt_submitColumn__0rY1R>button:focus{box-shadow:0 0 0 2px var(--brand-color-500)!important}.ChatPrompt_submitColumn__0rY1R>button:first-child{border:none;position:relative;transition:all .2s}.ChatPrompt_submitColumn__0rY1R>button:first-child:focus{transform:scale(1.2)}.ChatPrompt_submitColumn__0rY1R>button:first-child:before{bottom:-100%;content:\"\";left:-100%;position:absolute;right:-100%;top:-100%}.ChatPrompt_attachments__KG-fG{display:flex;flex-wrap:wrap;gap:var(--p-2);margin-bottom:var(--p-2)}.ChatPrompt_attachmentItem__QJk7J{flex:1 1 300px;max-width:300px;min-width:0}";
4
4
  var S = {"root":"ChatPrompt_root__5G5bq","composer":"ChatPrompt_composer__H3c3N","fileInput":"ChatPrompt_fileInput__xdgPn","attachButton":"ChatPrompt_attachButton__gi-qF","editorWrap":"ChatPrompt_editorWrap__Q7gat","editorMount":"ChatPrompt_editorMount__Phh4D","submitColumn":"ChatPrompt_submitColumn__0rY1R","attachments":"ChatPrompt_attachments__KG-fG","attachmentItem":"ChatPrompt_attachmentItem__QJk7J"};
5
5
  styleInject(css_248z);
6
6
 
@@ -5,7 +5,7 @@ import { PaperclipIcon, SendHorizontalIcon } from 'lucide-react';
5
5
  import { Button } from '../../Button/Button.js';
6
6
  import S from './ChatPrompt.styl.js';
7
7
 
8
- function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles, onComposerKeyDown, }) {
8
+ function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles, }) {
9
9
  const fileInputRef = useRef(null);
10
10
  const showAttachButton = Boolean(attachmentAccept && onAttachmentFiles);
11
11
  const handleFileInputChange = useCallback((e) => {
@@ -19,7 +19,7 @@ function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, att
19
19
  return (jsxs("div", { className: S.composer, children: [showAttachButton ? (jsxs(Fragment, { children: [jsx("input", { ref: fileInputRef, type: "file", accept: attachmentAccept, multiple: true, className: S.fileInput, disabled: disabled, onChange: handleFileInputChange }), jsx(Button, { type: "button", variant: "ghost", icon: true, size: "sm", className: S.attachButton, "aria-label": "Attach file", disabled: disabled, onClick: e => {
20
20
  e.preventDefault();
21
21
  fileInputRef.current?.click();
22
- }, children: jsx(PaperclipIcon, { size: 16 }) })] })) : null, jsx("div", { className: S.editorWrap, onKeyDown: onComposerKeyDown, children: jsx(EditorContent, { editor: editor, className: S.editorMount }) }), jsx("div", { className: S.submitColumn, children: jsx(Button, { type: "submit", size: "sm", disabled: disabled || !canSubmit, onMouseDown: e => e.preventDefault(), children: jsx(SendHorizontalIcon, { size: 16 }) }) })] }));
22
+ }, children: jsx(PaperclipIcon, { size: 16 }) })] })) : null, jsx("div", { className: S.editorWrap, children: jsx(EditorContent, { editor: editor, className: S.editorMount }) }), jsx("div", { className: S.submitColumn, children: jsx(Button, { type: "submit", size: "sm", disabled: disabled || !canSubmit, onMouseDown: e => e.preventDefault(), children: jsx(SendHorizontalIcon, { size: 16 }) }) })] }));
23
23
  }
24
24
 
25
25
  export { ChatPromptComposer };
@@ -44,7 +44,6 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
44
44
  Placeholder.configure({
45
45
  placeholder: placeholderText,
46
46
  showOnlyWhenEditable: true,
47
- showOnlyCurrent: false,
48
47
  }),
49
48
  ];
50
49
  if (slashItemsStable.length > 0) {
@@ -70,6 +69,28 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
70
69
  });
71
70
  }, []);
72
71
  const trimmedMessage = plainDraft.trim();
72
+ const trimmedMessageRef = useRef(trimmedMessage);
73
+ trimmedMessageRef.current = trimmedMessage;
74
+ const attachmentsCountRef = useRef(attachmentsCount);
75
+ attachmentsCountRef.current = attachmentsCount;
76
+ const onEnterSubmitRef = useRef(onEnterSubmit);
77
+ onEnterSubmitRef.current = onEnterSubmit;
78
+ const handleEditorKeyDown = useCallback((_view, event) => {
79
+ if (!(event.key === 'Enter' &&
80
+ !event.shiftKey &&
81
+ !event.metaKey &&
82
+ !event.ctrlKey)) {
83
+ return false;
84
+ }
85
+ if (slashOpenRef.current)
86
+ return false;
87
+ if (!trimmedMessageRef.current && attachmentsCountRef.current === 0) {
88
+ return false;
89
+ }
90
+ event.preventDefault();
91
+ onEnterSubmitRef.current();
92
+ return true;
93
+ }, []);
73
94
  const editor = useEditor({
74
95
  extensions,
75
96
  content: CHAT_PROMPT_EMPTY_DOC,
@@ -80,6 +101,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
80
101
  spellcheck: 'true',
81
102
  'aria-label': ariaLabelComposer,
82
103
  },
104
+ handleKeyDown: handleEditorKeyDown,
83
105
  },
84
106
  onTransaction: ({ editor: ed }) => {
85
107
  const dom = chatPromptSafeEditorDom(ed);
@@ -89,7 +111,7 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
89
111
  setPlainDraft(ed.getText());
90
112
  },
91
113
  onCreate: bindEditorDom,
92
- }, [extensions, bindEditorDom, ariaLabelComposer]);
114
+ }, [extensions, bindEditorDom, ariaLabelComposer, handleEditorKeyDown]);
93
115
  useEffect(() => {
94
116
  if (!editor)
95
117
  return;
@@ -131,8 +153,6 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
131
153
  return;
132
154
  syncChatPromptComposerHeight(dom, plainDraft);
133
155
  }, [editor, plainDraft]);
134
- const onEnterSubmitRef = useRef(onEnterSubmit);
135
- onEnterSubmitRef.current = onEnterSubmit;
136
156
  const resetAfterSend = useCallback(() => {
137
157
  if (!editor)
138
158
  return;
@@ -144,23 +164,10 @@ function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlash
144
164
  });
145
165
  setPlainDraft('');
146
166
  }, [editor]);
147
- const handleComposerKeyDown = useCallback((e) => {
148
- if (!(e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey))
149
- return;
150
- if (!editorDomRef.current?.contains(e.target))
151
- return;
152
- if (slashOpenRef.current)
153
- return;
154
- if (!trimmedMessage && attachmentsCount === 0)
155
- return;
156
- e.preventDefault();
157
- onEnterSubmitRef.current();
158
- }, [attachmentsCount, trimmedMessage]);
159
167
  return {
160
168
  editor,
161
169
  trimmedMessage,
162
170
  resetAfterSend,
163
- handleComposerKeyDown,
164
171
  };
165
172
  }
166
173
 
@@ -8,7 +8,7 @@ import S from './PageHeader.styl.js';
8
8
 
9
9
  function PageHeader({ breadcrumbs, breadcrumbClientLogo, breadcrumbCompanyName, breadcrumbSidebarTrigger = true, title, subheader, actions, }) {
10
10
  const { isScrolled } = useContext(PageContext);
11
- return (jsx("div", { className: cn(S.root, actions && S.hasActions, isScrolled && S.scrolled), children: jsxs("div", { className: S.inner, children: [jsx(Breadcrumbs, { className: S.breadcrumbs, items: breadcrumbs ?? [], clientLogo: breadcrumbClientLogo, companyName: breadcrumbCompanyName, sidebarTrigger: breadcrumbSidebarTrigger, children: isScrolled && (jsxs("div", { className: S.titleDupe, children: [jsx(BreadCrumbsSeparator, { size: 14 }), title] })) }), jsxs("div", { className: S.main, children: [jsxs("div", { className: S.title, children: [jsx("h1", { children: title }), subheader && (jsx(TextWithDeferTooltip, { className: S.subheader, children: subheader }))] }), actions && jsx("div", { className: S.actions, children: actions })] })] }) }));
11
+ return (jsx("div", { className: cn(S.root, actions && S.hasActions, isScrolled && S.scrolled), children: jsxs("div", { className: S.inner, children: [jsx(Breadcrumbs, { className: S.breadcrumbs, items: breadcrumbs ?? [], clientLogo: breadcrumbClientLogo, companyName: breadcrumbCompanyName, sidebarTrigger: breadcrumbSidebarTrigger, children: isScrolled && (jsxs("div", { className: S.titleDupe, children: [jsx(BreadCrumbsSeparator, { size: 14 }), title] })) }), jsxs("div", { className: S.main, children: [jsxs("div", { className: S.title, children: [jsx("h1", { children: title }), subheader && (jsx(TextWithDeferTooltip, { className: S.subheader, overTrigger: true, children: subheader }))] }), actions && jsx("div", { className: S.actions, children: actions })] })] }) }));
12
12
  }
13
13
 
14
14
  export { PageHeader };
@@ -4,19 +4,41 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip.js';
4
4
 
5
5
  function TextWithDeferTooltip({ className, children, width, maxWidth, side = 'bottom', overTrigger = false, ...props }) {
6
6
  const [withTooltip, setWithTooltip] = useState(false);
7
+ const [tooltipStyles, setTooltipStyles] = useState({});
7
8
  const ref = useRef(null);
9
+ const cachedFontSizeRef = useRef(null);
8
10
  const handleMouseEnter = () => {
9
11
  if (!ref.current)
10
12
  return;
11
13
  const isOverflowingHorizontally = ref.current.scrollWidth - ref.current.clientWidth > 3;
12
14
  const isOverflowingVertically = ref.current.scrollHeight - ref.current.clientHeight > 3;
13
15
  if (isOverflowingHorizontally || isOverflowingVertically) {
16
+ const styles = {
17
+ fontSize: cachedFontSizeRef.current || undefined,
18
+ };
19
+ if (ref.current) {
20
+ const { width: rectWidth, left, top, } = ref.current.getBoundingClientRect();
21
+ styles.width = `${width ?? rectWidth}px`;
22
+ if (!cachedFontSizeRef.current) {
23
+ const { fontSize } = window.getComputedStyle(ref.current);
24
+ cachedFontSizeRef.current = fontSize;
25
+ styles.fontSize = fontSize;
26
+ }
27
+ if (overTrigger) {
28
+ styles.transform = `translate(${left}px, ${top}px) !important`;
29
+ }
30
+ }
31
+ setTooltipStyles(styles);
14
32
  setWithTooltip(true);
15
33
  }
16
34
  };
17
35
  const textElement = (jsx("div", { ref: ref, className: className, onMouseEnter: handleMouseEnter, ...props, children: children }));
18
36
  if (withTooltip) {
19
- return (jsxs(Tooltip, { open: withTooltip, onOpenChange: setWithTooltip, children: [jsx(TooltipTrigger, { asChild: true, children: textElement }), jsx(TooltipContent, { side: side, overTrigger: overTrigger, maxWidth: maxWidth, style: width !== undefined ? { width: `${width}px` } : undefined, children: children })] }));
37
+ const tooltipSide = overTrigger ? 'bottom' : side;
38
+ return (jsxs(Tooltip, { open: withTooltip, onOpenChange: setWithTooltip, children: [jsx(TooltipTrigger, { asChild: true, children: textElement }), jsx(TooltipContent, { side: tooltipSide, style: {
39
+ ...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
40
+ ...tooltipStyles,
41
+ }, overTrigger: overTrigger, children: children })] }));
20
42
  }
21
43
  return textElement;
22
44
  }
@@ -83,7 +83,7 @@ function DriverCard({ selectedDriver, isLoading, inQueue = false, driverSelector
83
83
  const directionText = direction > 0 ? 'Positive' : 'Negative';
84
84
  const DirectionIcon = direction > 0 ? TrendUpIcon : TrendDownIcon;
85
85
  const nameElem = (jsx("h4", { className: `${S.driverTitle} ${S.truncated}`, children: name }));
86
- return (jsx(Card, { className: S.root, paddingSize: "l", children: jsx(CardContent, { noScroll: true, children: jsxs("div", { className: S.cardContent, children: [jsx("div", { className: S.driverHeader, children: jsxs("div", { className: S.headerContent, children: [jsxs("div", { className: S.topHeader, children: [jsxs("p", { className: S.categoryInfo, children: [jsx("span", { className: S.categoryIcon, children: getCategoryIcon(category) }), jsx("span", { className: S.categoryText, children: category })] }), driverSelector] }), name.length > 60 ? (jsxs(Tooltip, { children: [jsx(LabelWithId, { id: id, label: jsx(TooltipTrigger, { asChild: true, children: nameElem }) }), jsx(TooltipContent, { side: "left", className: S.tooltipContent, children: jsx("div", { className: S.tooltipTitle, children: name }) })] })) : (jsx(LabelWithId, { id: id, label: nameElem })), jsx("p", { className: S.regionDisplay, children: regionDisplay })] }) }), jsx("div", { className: S.metricsSection, children: jsx("div", { className: S.importanceScore, children: importanceDisplay }) }), jsxs("div", { className: S.directionLagSection, children: [jsxs(Badge, { variant: direction > 0 ? 'green' : 'red', className: S.directionBadge, children: [jsx(DirectionIcon, { className: S.trendIcon }), directionText, " correlation"] }), jsxs("span", { className: S.lagInfo, children: ["Lag: ", lag] })] }), jsx(DriverPerformanceChart, { driver: selectedDriver }), jsx("p", { className: S.description, children: summary ?? '' })] }) }) }));
86
+ return (jsx(Card, { className: S.root, paddingSize: "l", children: jsx(CardContent, { noScroll: true, children: jsxs("div", { className: S.cardContent, children: [jsx("div", { className: S.driverHeader, children: jsxs("div", { className: S.headerContent, children: [jsxs("div", { className: S.topHeader, children: [jsxs("p", { className: S.categoryInfo, children: [jsx("span", { className: S.categoryIcon, children: getCategoryIcon(category) }), jsx("span", { className: S.categoryText, children: category })] }), driverSelector] }), name.length > 60 ? (jsxs(Tooltip, { children: [jsx(LabelWithId, { id: id, label: jsx(TooltipTrigger, { asChild: true, children: nameElem }) }), jsx(TooltipContent, { side: "left", className: S.tooltipContent, overTrigger: true, children: jsx("div", { className: S.tooltipTitle, children: name }) })] })) : (jsx(LabelWithId, { id: id, label: nameElem })), jsx("p", { className: S.regionDisplay, children: regionDisplay })] }) }), jsx("div", { className: S.metricsSection, children: jsx("div", { className: S.importanceScore, children: importanceDisplay }) }), jsxs("div", { className: S.directionLagSection, children: [jsxs(Badge, { variant: direction > 0 ? 'green' : 'red', className: S.directionBadge, children: [jsx(DirectionIcon, { className: S.trendIcon }), directionText, " correlation"] }), jsxs("span", { className: S.lagInfo, children: ["Lag: ", lag] })] }), jsx(DriverPerformanceChart, { driver: selectedDriver }), jsx("p", { className: S.description, children: summary ?? '' })] }) }) }));
87
87
  }
88
88
 
89
89
  export { DriverCard };
@@ -1,4 +1,3 @@
1
- import { type KeyboardEvent as ReactKeyboardEvent } from 'react';
2
1
  import type { Editor } from '@tiptap/core';
3
2
  import type { ChatAttachmentDropItem } from '../Chat.types';
4
3
  export type ChatPromptComposerProps = {
@@ -8,6 +7,5 @@ export type ChatPromptComposerProps = {
8
7
  attachments: ChatAttachmentDropItem[];
9
8
  attachmentAccept?: string;
10
9
  onAttachmentFiles?: (files: File[]) => void;
11
- onComposerKeyDown: (event: ReactKeyboardEvent) => void;
12
10
  };
13
- export declare function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles, onComposerKeyDown, }: ChatPromptComposerProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function ChatPromptComposer({ editor, disabled, trimmedMessage, attachments, attachmentAccept, onAttachmentFiles, }: ChatPromptComposerProps): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,3 @@
1
- import { type KeyboardEvent as ReactKeyboardEvent } from 'react';
2
1
  import type { SlashCommandItem, SlashOnItemCommand } from '#uilib/tiptap/slash-mention/types';
3
2
  import type { Editor } from '@tiptap/core';
4
3
  export type UseChatPromptEditorOptions = {
@@ -16,6 +15,5 @@ export type UseChatPromptEditorResult = {
16
15
  editor: Editor | null;
17
16
  trimmedMessage: string;
18
17
  resetAfterSend: () => void;
19
- handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
20
18
  };
21
19
  export declare function useChatPromptEditor({ disabled, placeholder, slashCommandItems, onSlashItemCommand, prefillMessage, attachmentsCount, onEnterSubmit, }: UseChatPromptEditorOptions): UseChatPromptEditorResult;
@@ -0,0 +1 @@
1
+ export default function ChatSheetPage(): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.43",
3
+ "version": "1.3.44",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -62,7 +62,7 @@ INPUT_MAX_HEIGHT = 200px
62
62
  overflow-y auto !important
63
63
  overflow-x hidden !important
64
64
 
65
- & :global(.ProseMirror p.is-empty::before)
65
+ & :global(.ProseMirror p.is-empty.is-editor-empty::before)
66
66
  color var(--muted-foreground)
67
67
  content attr(data-placeholder)
68
68
  float left
@@ -24,16 +24,15 @@ export function ChatPrompt({
24
24
  const attachmentsCount = attachments.length;
25
25
 
26
26
  const emitSubmitRef = useRef(() => {});
27
- const { editor, trimmedMessage, resetAfterSend, handleComposerKeyDown } =
28
- useChatPromptEditor({
29
- disabled,
30
- placeholder,
31
- slashCommandItems,
32
- onSlashItemCommand,
33
- prefillMessage,
34
- attachmentsCount,
35
- onEnterSubmit: () => emitSubmitRef.current(),
36
- });
27
+ const { editor, trimmedMessage, resetAfterSend } = useChatPromptEditor({
28
+ disabled,
29
+ placeholder,
30
+ slashCommandItems,
31
+ onSlashItemCommand,
32
+ prefillMessage,
33
+ attachmentsCount,
34
+ onEnterSubmit: () => emitSubmitRef.current(),
35
+ });
37
36
 
38
37
  const emitSubmitAndClear = useCallback(() => {
39
38
  if (!editor) return;
@@ -79,7 +78,6 @@ export function ChatPrompt({
79
78
  attachments={attachments}
80
79
  attachmentAccept={attachmentAccept}
81
80
  onAttachmentFiles={onAttachmentFiles}
82
- onComposerKeyDown={handleComposerKeyDown}
83
81
  />
84
82
  {footer}
85
83
  </form>
@@ -1,9 +1,4 @@
1
- import {
2
- type ChangeEvent,
3
- type KeyboardEvent as ReactKeyboardEvent,
4
- useCallback,
5
- useRef,
6
- } from 'react';
1
+ import { type ChangeEvent, useCallback, useRef } from 'react';
7
2
 
8
3
  import type { Editor } from '@tiptap/core';
9
4
  import { EditorContent } from '@tiptap/react';
@@ -20,7 +15,6 @@ export type ChatPromptComposerProps = {
20
15
  attachments: ChatAttachmentDropItem[];
21
16
  attachmentAccept?: string;
22
17
  onAttachmentFiles?: (files: File[]) => void;
23
- onComposerKeyDown: (event: ReactKeyboardEvent) => void;
24
18
  };
25
19
 
26
20
  export function ChatPromptComposer({
@@ -30,7 +24,6 @@ export function ChatPromptComposer({
30
24
  attachments,
31
25
  attachmentAccept,
32
26
  onAttachmentFiles,
33
- onComposerKeyDown,
34
27
  }: ChatPromptComposerProps) {
35
28
  const fileInputRef = useRef<HTMLInputElement>(null);
36
29
  const showAttachButton = Boolean(attachmentAccept && onAttachmentFiles);
@@ -79,7 +72,7 @@ export function ChatPromptComposer({
79
72
  </>
80
73
  ) : null}
81
74
 
82
- <div className={S.editorWrap} onKeyDown={onComposerKeyDown}>
75
+ <div className={S.editorWrap}>
83
76
  <EditorContent editor={editor} className={S.editorMount} />
84
77
  </div>
85
78
 
@@ -1,5 +1,4 @@
1
1
  import {
2
- type KeyboardEvent as ReactKeyboardEvent,
3
2
  useCallback,
4
3
  useEffect,
5
4
  useLayoutEffect,
@@ -43,7 +42,6 @@ export type UseChatPromptEditorResult = {
43
42
  editor: Editor | null;
44
43
  trimmedMessage: string;
45
44
  resetAfterSend: () => void;
46
- handleComposerKeyDown: (event: ReactKeyboardEvent) => void;
47
45
  };
48
46
 
49
47
  export function useChatPromptEditor({
@@ -94,7 +92,6 @@ export function useChatPromptEditor({
94
92
  Placeholder.configure({
95
93
  placeholder: placeholderText,
96
94
  showOnlyWhenEditable: true,
97
- showOnlyCurrent: false,
98
95
  }),
99
96
  ];
100
97
  if (slashItemsStable.length > 0) {
@@ -126,6 +123,38 @@ export function useChatPromptEditor({
126
123
 
127
124
  const trimmedMessage = plainDraft.trim();
128
125
 
126
+ const trimmedMessageRef = useRef(trimmedMessage);
127
+ trimmedMessageRef.current = trimmedMessage;
128
+
129
+ const attachmentsCountRef = useRef(attachmentsCount);
130
+ attachmentsCountRef.current = attachmentsCount;
131
+
132
+ const onEnterSubmitRef = useRef(onEnterSubmit);
133
+ onEnterSubmitRef.current = onEnterSubmit;
134
+
135
+ const handleEditorKeyDown = useCallback(
136
+ (_view: unknown, event: KeyboardEvent) => {
137
+ if (
138
+ !(
139
+ event.key === 'Enter' &&
140
+ !event.shiftKey &&
141
+ !event.metaKey &&
142
+ !event.ctrlKey
143
+ )
144
+ ) {
145
+ return false;
146
+ }
147
+ if (slashOpenRef.current) return false;
148
+ if (!trimmedMessageRef.current && attachmentsCountRef.current === 0) {
149
+ return false;
150
+ }
151
+ event.preventDefault();
152
+ onEnterSubmitRef.current();
153
+ return true;
154
+ },
155
+ [],
156
+ );
157
+
129
158
  const editor = useEditor(
130
159
  {
131
160
  extensions,
@@ -137,6 +166,7 @@ export function useChatPromptEditor({
137
166
  spellcheck: 'true',
138
167
  'aria-label': ariaLabelComposer,
139
168
  },
169
+ handleKeyDown: handleEditorKeyDown,
140
170
  },
141
171
  onTransaction: ({ editor: ed }) => {
142
172
  const dom = chatPromptSafeEditorDom(ed);
@@ -147,7 +177,7 @@ export function useChatPromptEditor({
147
177
  },
148
178
  onCreate: bindEditorDom,
149
179
  },
150
- [extensions, bindEditorDom, ariaLabelComposer],
180
+ [extensions, bindEditorDom, ariaLabelComposer, handleEditorKeyDown],
151
181
  );
152
182
 
153
183
  useEffect(() => {
@@ -188,9 +218,6 @@ export function useChatPromptEditor({
188
218
  syncChatPromptComposerHeight(dom, plainDraft);
189
219
  }, [editor, plainDraft]);
190
220
 
191
- const onEnterSubmitRef = useRef(onEnterSubmit);
192
- onEnterSubmitRef.current = onEnterSubmit;
193
-
194
221
  const resetAfterSend = useCallback(() => {
195
222
  if (!editor) return;
196
223
  editor.chain().clearContent().focus().run();
@@ -201,23 +228,9 @@ export function useChatPromptEditor({
201
228
  setPlainDraft('');
202
229
  }, [editor]);
203
230
 
204
- const handleComposerKeyDown = useCallback(
205
- (e: ReactKeyboardEvent) => {
206
- if (!(e.key === 'Enter' && !e.shiftKey && !e.metaKey && !e.ctrlKey))
207
- return;
208
- if (!editorDomRef.current?.contains(e.target as Node)) return;
209
- if (slashOpenRef.current) return;
210
- if (!trimmedMessage && attachmentsCount === 0) return;
211
- e.preventDefault();
212
- onEnterSubmitRef.current();
213
- },
214
- [attachmentsCount, trimmedMessage],
215
- );
216
-
217
231
  return {
218
232
  editor,
219
233
  trimmedMessage,
220
234
  resetAfterSend,
221
- handleComposerKeyDown,
222
235
  };
223
236
  }
@@ -54,7 +54,7 @@ export function PageHeader({
54
54
  <div className={S.title}>
55
55
  <h1>{title}</h1>
56
56
  {subheader && (
57
- <TextWithDeferTooltip className={S.subheader}>
57
+ <TextWithDeferTooltip className={S.subheader} overTrigger>
58
58
  {subheader}
59
59
  </TextWithDeferTooltip>
60
60
  )}
@@ -1,4 +1,4 @@
1
- import { useRef, useState } from 'react';
1
+ import { CSSProperties, useRef, useState } from 'react';
2
2
 
3
3
  import {
4
4
  Tooltip,
@@ -18,7 +18,9 @@ function TextWithDeferTooltip({
18
18
  ...props
19
19
  }: TextWithDeferTooltipProps) {
20
20
  const [withTooltip, setWithTooltip] = useState(false);
21
+ const [tooltipStyles, setTooltipStyles] = useState<CSSProperties>({});
21
22
  const ref = useRef<HTMLDivElement>(null);
23
+ const cachedFontSizeRef = useRef<string | null>(null);
22
24
 
23
25
  const handleMouseEnter = () => {
24
26
  if (!ref.current) return;
@@ -29,6 +31,31 @@ function TextWithDeferTooltip({
29
31
  ref.current.scrollHeight - ref.current.clientHeight > 3;
30
32
 
31
33
  if (isOverflowingHorizontally || isOverflowingVertically) {
34
+ const styles: CSSProperties = {
35
+ fontSize: cachedFontSizeRef.current || undefined,
36
+ };
37
+
38
+ if (ref.current) {
39
+ const {
40
+ width: rectWidth,
41
+ left,
42
+ top,
43
+ } = ref.current.getBoundingClientRect();
44
+
45
+ styles.width = `${width ?? rectWidth}px`;
46
+
47
+ if (!cachedFontSizeRef.current) {
48
+ const { fontSize } = window.getComputedStyle(ref.current);
49
+ cachedFontSizeRef.current = fontSize;
50
+ styles.fontSize = fontSize;
51
+ }
52
+
53
+ if (overTrigger) {
54
+ styles.transform = `translate(${left}px, ${top}px) !important`;
55
+ }
56
+ }
57
+
58
+ setTooltipStyles(styles);
32
59
  setWithTooltip(true);
33
60
  }
34
61
  };
@@ -45,14 +72,18 @@ function TextWithDeferTooltip({
45
72
  );
46
73
 
47
74
  if (withTooltip) {
75
+ const tooltipSide = overTrigger ? 'bottom' : side;
76
+
48
77
  return (
49
78
  <Tooltip open={withTooltip} onOpenChange={setWithTooltip}>
50
79
  <TooltipTrigger asChild>{textElement}</TooltipTrigger>
51
80
  <TooltipContent
52
- side={side}
81
+ side={tooltipSide}
82
+ style={{
83
+ ...(maxWidth !== undefined && { maxWidth: `${maxWidth}px` }),
84
+ ...tooltipStyles,
85
+ }}
53
86
  overTrigger={overTrigger}
54
- maxWidth={maxWidth}
55
- style={width !== undefined ? { width: `${width}px` } : undefined}
56
87
  >
57
88
  {children}
58
89
  </TooltipContent>
@@ -177,7 +177,11 @@ export function DriverCard({
177
177
  id={id}
178
178
  label={<TooltipTrigger asChild>{nameElem}</TooltipTrigger>}
179
179
  />
180
- <TooltipContent side="left" className={S.tooltipContent}>
180
+ <TooltipContent
181
+ side="left"
182
+ className={S.tooltipContent}
183
+ overTrigger
184
+ >
181
185
  <div className={S.tooltipTitle}>{name}</div>
182
186
  </TooltipContent>
183
187
  </Tooltip>
@@ -0,0 +1,61 @@
1
+ import type { SlashCommandItem } from '#uilib/components/ui/Chat';
2
+ import { PageContentSection } from '#uilib/components/ui/Page';
3
+ import { MessageSquare } from 'lucide-react';
4
+
5
+ import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
6
+ import { DOCS_CHAT_USER_KEY } from '../docsConstants';
7
+ import { DocsHeaderActions } from '../docsHeaderActions';
8
+
9
+ const DOCS_CHAT_SHEET_SCOPE_ID = `${DOCS_CHAT_USER_KEY}-docs-chat-sheet-portal`;
10
+
11
+ const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
12
+ {
13
+ id: 'sample-command',
14
+ label: 'sample-command',
15
+ description: 'Demo slash item for portal ChatSheet regression.',
16
+ },
17
+ ];
18
+
19
+ export default function ChatSheetPage() {
20
+ return (
21
+ <>
22
+ <AppPageHeader
23
+ breadcrumbs={[{ label: 'Chat' }, { label: 'Chat sheet' }]}
24
+ title="Chat sheet (portal)"
25
+ subheader="Same integration as design-demo: ChatSheet in page header actions, chat panel portaled into the shell sidebar slot. No inline ChatChrome on this page."
26
+ actions={
27
+ <DocsHeaderActions
28
+ scopeId={DOCS_CHAT_SHEET_SCOPE_ID}
29
+ slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
30
+ triggerLabel={
31
+ <>
32
+ <MessageSquare size={20} />
33
+ &nbsp;AI Assistant
34
+ </>
35
+ }
36
+ />
37
+ }
38
+ />
39
+ <PageContentSection>
40
+ <p style={{ marginBottom: 16, fontSize: 14, lineHeight: 1.5 }}>
41
+ Open <strong>AI Assistant</strong> in the header. The composer runs
42
+ inside the portaled chat panel (not inline on this page).
43
+ </p>
44
+ <h3 style={{ marginBottom: 8, fontSize: 14, fontWeight: 600 }}>
45
+ Regression checklist
46
+ </h3>
47
+ <ul
48
+ style={{ margin: 0, paddingLeft: 20, fontSize: 14, lineHeight: 1.6 }}
49
+ >
50
+ <li>Plain Enter with text → submit and clear composer</li>
51
+ <li>
52
+ Plain Enter must not create empty lines with duplicated placeholders
53
+ </li>
54
+ <li>Shift+Enter with text → multiline, no placeholder after text</li>
55
+ <li>Empty Enter → no submit</li>
56
+ <li>Submit button and slash menu still work</li>
57
+ </ul>
58
+ </PageContentSection>
59
+ </>
60
+ );
61
+ }
@@ -11,9 +11,6 @@ import { DocsHeaderActions } from '../docsHeaderActions';
11
11
 
12
12
  const TOOLTIP_SIDES = ['left', 'top', 'bottom', 'right'] as const;
13
13
 
14
- const OVER_TRIGGER_TEXT =
15
- 'Actual Order volume for Baerlocher MB 301, a compound that includes stearin as a component. MB 301 is typically used in construction-grade applications.';
16
-
17
14
  export default function TooltipPage() {
18
15
  return (
19
16
  <>
@@ -37,34 +34,6 @@ export default function TooltipPage() {
37
34
  ))}
38
35
  </div>
39
36
  </PageContentSection>
40
- <PageContentSection>
41
- <p style={{ margin: '0 0 8px', fontWeight: 600 }}>Over trigger</p>
42
- <div
43
- style={{
44
- maxWidth: 500,
45
- border: '1px dashed var(--border)',
46
- padding: 8,
47
- }}
48
- >
49
- <Tooltip>
50
- <TooltipTrigger asChild>
51
- <div
52
- style={{
53
- overflow: 'hidden',
54
- textOverflow: 'ellipsis',
55
- whiteSpace: 'nowrap',
56
- fontWeight: 600,
57
- }}
58
- >
59
- {OVER_TRIGGER_TEXT}
60
- </div>
61
- </TooltipTrigger>
62
- <TooltipContent overTrigger maxWidth={400}>
63
- {OVER_TRIGGER_TEXT}
64
- </TooltipContent>
65
- </Tooltip>
66
- </div>
67
- </PageContentSection>
68
37
  </>
69
38
  );
70
39
  }
@@ -115,6 +115,12 @@ export const DOC_REGISTRY: DocEntry[] = [
115
115
  section: 'Chat',
116
116
  load: () => import('./pages/ChatSlashCommandsPage'),
117
117
  },
118
+ {
119
+ slug: 'chat-sheet',
120
+ title: 'Chat sheet (portal)',
121
+ section: 'Chat',
122
+ load: () => import('./pages/ChatSheetPage'),
123
+ },
118
124
  {
119
125
  slug: 'chat-user-csv-attachment',
120
126
  title: 'Chat user CSV attachment',