@sybilion/uilib 1.3.59 → 1.3.61

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.
@@ -13,7 +13,7 @@ function ChatPresets({ chatId, items, usedItemIds = [], layout = 'fixed', onSele
13
13
  const availableItems = items.filter(item => !usedItems.includes(item.id));
14
14
  if (availableItems.length === 0)
15
15
  return null;
16
- return (jsx("div", { className: layout === 'inline' ? S.inlineRoot : S.root, children: jsx("div", { className: cn(S.inner, layout === 'inline' && S.innerInline), children: availableItems.map(preset => (jsx(Button, { type: "button", className: cn(S.item), onClick: () => {
16
+ return (jsx("div", { className: layout === 'inline' ? S.inlineRoot : S.root, children: jsx("div", { className: cn(S.inner, layout === 'inline' && S.innerInline), children: availableItems.map(preset => (jsx(Button, { type: "button", variant: "outline", size: "sm", className: cn(S.item), onClick: () => {
17
17
  onSelect?.(preset);
18
18
  onItemUsed?.(preset.id);
19
19
  setLocalUsedItems(prev => [...prev, preset.id]);
@@ -1,6 +1,6 @@
1
1
  import styleInject from 'style-inject';
2
2
 
3
- var css_248z = ".ChatPresets_root__Cj42o{bottom:160px;width:100%}.ChatPresets_inlineRoot__WXVnu{margin-top:var(--p-6);position:relative;width:100%}.ChatPresets_inner__h14-q{background-color:var(--background);display:flex;flex-wrap:wrap;gap:8px;padding:var(--p-2) var(--p-6) var(--p-3)}.ChatPresets_innerInline__iPM2b{background-color:transparent}.ChatPresets_item__LfX5b{background:none;border:1px solid var(--border);border-radius:var(--p-4);color:var(--foreground);cursor:pointer;flex-shrink:0;font-size:var(--text-xs);line-height:1.4;max-width:300px;padding:var(--p-3);text-align:left;transition:all .2s ease;white-space:break-spaces}.ChatPresets_item__LfX5b:hover{background:var(--border)!important}.dark .ChatPresets_item__LfX5b{background-color:var(--muted)}";
3
+ var css_248z = ".ChatPresets_root__Cj42o{bottom:160px;width:100%}.ChatPresets_inlineRoot__WXVnu{margin-top:var(--p-6);position:relative;width:100%}.ChatPresets_inner__h14-q{background-color:var(--background);display:flex;flex-wrap:wrap;gap:8px;padding:var(--p-2) var(--p-6) var(--p-3)}.ChatPresets_innerInline__iPM2b{background-color:transparent}.ChatPresets_item__LfX5b{flex-shrink:0;font-size:var(--text-xs);height:auto;line-height:1.4;max-width:300px;min-height:auto;min-width:0;overflow-wrap:anywhere;padding:var(--p-3);text-align:left;white-space:break-spaces!important}";
4
4
  var S = {"root":"ChatPresets_root__Cj42o","inlineRoot":"ChatPresets_inlineRoot__WXVnu","inner":"ChatPresets_inner__h14-q","innerInline":"ChatPresets_innerInline__iPM2b","item":"ChatPresets_item__LfX5b"};
5
5
  styleInject(css_248z);
6
6
 
@@ -123,29 +123,27 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
123
123
  if (embedAsPage) {
124
124
  return;
125
125
  }
126
- const run = () => {
127
- setIsOpen(open);
128
- setChatPanelOpen(open);
129
- };
130
- if (!isMobile &&
131
- 'startViewTransition' in document &&
132
- document.startViewTransition) {
133
- document.startViewTransition(run);
134
- }
135
- else {
136
- run();
137
- }
138
126
  if (open) {
139
- // Avoid a second setSearchParams when `chat` is already set (e.g. Reports merges
140
- // reportId + chat in one update). A redundant add can merge from a stale `prev`
141
- // and drop other query keys.
127
+ setIsOpen(true);
128
+ setChatPanelOpen(true);
142
129
  if (!searchParams.has(CHAT_QUERY_PARAM)) {
143
130
  addSearchParams({ [CHAT_QUERY_PARAM]: CHAT_OPEN_VALUE });
144
131
  }
132
+ return;
133
+ }
134
+ const applyClose = () => {
135
+ setIsOpen(false);
136
+ setChatPanelOpen(false);
137
+ };
138
+ if (!isMobile &&
139
+ 'startViewTransition' in document &&
140
+ document.startViewTransition) {
141
+ document.startViewTransition(applyClose);
145
142
  }
146
143
  else {
147
- removeSearchParams(CHAT_QUERY_PARAM);
144
+ applyClose();
148
145
  }
146
+ removeSearchParams(CHAT_QUERY_PARAM);
149
147
  };
150
148
  const isEmpty = isChatEmpty(chat) && !isLoading;
151
149
  const openNewChatWithPrefill = useCallback((prompt) => {
@@ -630,9 +628,25 @@ function useChatPanelChromeModel({ embedAsPage, presets, scopeId, onMessage, onS
630
628
  if (embedAsPage) {
631
629
  return;
632
630
  }
633
- setIsOpen(chatOpen);
634
- setChatPanelOpen(chatOpen);
635
- }, [embedAsPage, chatOpen, setChatPanelOpen]);
631
+ if (!chatOpen) {
632
+ setIsOpen(false);
633
+ setChatPanelOpen(false);
634
+ return;
635
+ }
636
+ if (!shellChatPanelOpen && sidebarNavOpen) {
637
+ removeSearchParams(CHAT_QUERY_PARAM);
638
+ return;
639
+ }
640
+ setIsOpen(true);
641
+ setChatPanelOpen(true);
642
+ }, [
643
+ embedAsPage,
644
+ chatOpen,
645
+ setChatPanelOpen,
646
+ sidebarNavOpen,
647
+ shellChatPanelOpen,
648
+ removeSearchParams,
649
+ ]);
636
650
  /** Shell closed chat for space (e.g. nav opened); keep UI and URL in sync. */
637
651
  useEffect(() => {
638
652
  if (embedAsPage || shellChatPanelOpen || !isOpen) {
@@ -46,13 +46,24 @@ function clampChatWidthPx(px, shellWidth, effectiveSidebarWidthPx) {
46
46
  function defaultChatWidthPx(shellWidth) {
47
47
  return clampChatWidthPx(CHAT_WIDTH_DEFAULT_PX, shellWidth, SIDEBAR_WIDTH_DEFAULT_PX);
48
48
  }
49
+ /**
50
+ * Stored width can fall below panel min when the shell was narrow (clamp cap < min).
51
+ * For dual-panel fit checks, treat open panels as at least their min width.
52
+ */
53
+ function effectiveOpenPanelWidthPx(px, minPx, bothPanelsOpen) {
54
+ if (bothPanelsOpen) {
55
+ return Math.max(px, minPx);
56
+ }
57
+ return px > 0 ? px : minPx;
58
+ }
49
59
  /** Minimum shell width when nav and/or chat panels are open (main column at least `CENTRAL_AREA_MIN_PX`). */
50
60
  function requiredShellWidthForSidebars(widths) {
61
+ const bothPanelsOpen = widths.mainSidebarOpen && widths.chatPanelOpen;
51
62
  const sidebarW = widths.mainSidebarOpen
52
- ? widths.sidebarWidthPx || SIDEBAR_WIDTH_MIN_PX
63
+ ? effectiveOpenPanelWidthPx(widths.sidebarWidthPx, SIDEBAR_WIDTH_MIN_PX, bothPanelsOpen)
53
64
  : 0;
54
65
  const chatW = widths.chatPanelOpen
55
- ? widths.chatWidthPx || CHAT_WIDTH_MIN_PX
66
+ ? effectiveOpenPanelWidthPx(widths.chatWidthPx, CHAT_WIDTH_MIN_PX, bothPanelsOpen)
56
67
  : 0;
57
68
  return sidebarW + CENTRAL_AREA_MIN_PX + chatW;
58
69
  }
@@ -0,0 +1,5 @@
1
+ import type { ChatPreset } from '#uilib/components/ui/Chat/Chat.types';
2
+ /** Sample dataset name — mirrors app dataset page chat presets. */
3
+ export declare const DOCS_SAMPLE_DATASET_NAME = "HDPE Spot Price";
4
+ export declare function getDocsDatasetPresets(datasetName?: string): ChatPreset[];
5
+ export declare const DOCS_DATASET_PRESETS: ChatPreset[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.3.59",
3
+ "version": "1.3.61",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -20,22 +20,13 @@
20
20
 
21
21
  .item
22
22
  flex-shrink 0
23
+ min-width 0
23
24
  max-width 300px
25
+ height auto
26
+ min-height auto
24
27
  padding var(--p-3)
25
-
26
- background none
27
- border 1px solid var(--border)
28
- border-radius var(--p-4)
29
- cursor pointer
30
- transition all 0.2s ease
31
28
  text-align left
32
29
  font-size var(--text-xs)
33
30
  line-height 1.4
34
- white-space break-spaces
35
- color var(--foreground)
36
-
37
- &:hover
38
- background var(--border) !important
39
-
40
- :global(.dark) &
41
- background-color var(--muted)
31
+ white-space break-spaces !important
32
+ overflow-wrap anywhere
@@ -43,6 +43,8 @@ export function ChatPresets({
43
43
  <Button
44
44
  key={preset.id}
45
45
  type="button"
46
+ variant="outline"
47
+ size="sm"
46
48
  className={cn(S.item)}
47
49
  onClick={() => {
48
50
  onSelect?.(preset);
@@ -287,30 +287,29 @@ export function useChatPanelChromeModel({
287
287
  if (embedAsPage) {
288
288
  return;
289
289
  }
290
- const run = () => {
291
- setIsOpen(open);
292
- setChatPanelOpen(open);
290
+ if (open) {
291
+ setIsOpen(true);
292
+ setChatPanelOpen(true);
293
+ if (!searchParams.has(CHAT_QUERY_PARAM)) {
294
+ addSearchParams({ [CHAT_QUERY_PARAM]: CHAT_OPEN_VALUE });
295
+ }
296
+ return;
297
+ }
298
+
299
+ const applyClose = () => {
300
+ setIsOpen(false);
301
+ setChatPanelOpen(false);
293
302
  };
294
303
  if (
295
304
  !isMobile &&
296
305
  'startViewTransition' in document &&
297
306
  document.startViewTransition
298
307
  ) {
299
- document.startViewTransition(run);
308
+ document.startViewTransition(applyClose);
300
309
  } else {
301
- run();
302
- }
303
-
304
- if (open) {
305
- // Avoid a second setSearchParams when `chat` is already set (e.g. Reports merges
306
- // reportId + chat in one update). A redundant add can merge from a stale `prev`
307
- // and drop other query keys.
308
- if (!searchParams.has(CHAT_QUERY_PARAM)) {
309
- addSearchParams({ [CHAT_QUERY_PARAM]: CHAT_OPEN_VALUE });
310
- }
311
- } else {
312
- removeSearchParams(CHAT_QUERY_PARAM);
310
+ applyClose();
313
311
  }
312
+ removeSearchParams(CHAT_QUERY_PARAM);
314
313
  };
315
314
 
316
315
  const isEmpty = isChatEmpty(chat) && !isLoading;
@@ -856,9 +855,25 @@ export function useChatPanelChromeModel({
856
855
  if (embedAsPage) {
857
856
  return;
858
857
  }
859
- setIsOpen(chatOpen);
860
- setChatPanelOpen(chatOpen);
861
- }, [embedAsPage, chatOpen, setChatPanelOpen]);
858
+ if (!chatOpen) {
859
+ setIsOpen(false);
860
+ setChatPanelOpen(false);
861
+ return;
862
+ }
863
+ if (!shellChatPanelOpen && sidebarNavOpen) {
864
+ removeSearchParams(CHAT_QUERY_PARAM);
865
+ return;
866
+ }
867
+ setIsOpen(true);
868
+ setChatPanelOpen(true);
869
+ }, [
870
+ embedAsPage,
871
+ chatOpen,
872
+ setChatPanelOpen,
873
+ sidebarNavOpen,
874
+ shellChatPanelOpen,
875
+ removeSearchParams,
876
+ ]);
862
877
 
863
878
  /** Shell closed chat for space (e.g. nav opened); keep UI and URL in sync. */
864
879
  useEffect(() => {
@@ -0,0 +1,25 @@
1
+ import type { ChatPreset } from '#uilib/components/ui/Chat/Chat.types';
2
+
3
+ /** Sample dataset name — mirrors app dataset page chat presets. */
4
+ export const DOCS_SAMPLE_DATASET_NAME = 'HDPE Spot Price';
5
+
6
+ export function getDocsDatasetPresets(
7
+ datasetName: string = DOCS_SAMPLE_DATASET_NAME,
8
+ ): ChatPreset[] {
9
+ return [
10
+ {
11
+ id: '1',
12
+ text: `What should my planning decisions be based on the "${datasetName}" forecasts?`,
13
+ },
14
+ {
15
+ id: '2',
16
+ text: `Explain the "${datasetName}" forecast.`,
17
+ },
18
+ {
19
+ id: '3',
20
+ text: `What is the "${datasetName}" forecast performance?`,
21
+ },
22
+ ];
23
+ }
24
+
25
+ export const DOCS_DATASET_PRESETS = getDocsDatasetPresets();
@@ -2,11 +2,15 @@ import type { SlashCommandItem } from '#uilib/components/ui/Chat';
2
2
  import { PageContentSection } from '#uilib/components/ui/Page';
3
3
  import { MessageSquare } from 'lucide-react';
4
4
 
5
+ import {
6
+ DOCS_DATASET_PRESETS,
7
+ DOCS_SAMPLE_DATASET_NAME,
8
+ } from '../chatPresets.sample';
5
9
  import { AppPageHeader } from '../components/AppPageHeader/AppPageHeader';
6
10
  import { DOCS_CHAT_USER_KEY } from '../docsConstants';
7
11
  import { DocsHeaderActions } from '../docsHeaderActions';
8
12
 
9
- const DOCS_CHAT_SHEET_SCOPE_ID = `${DOCS_CHAT_USER_KEY}-docs-chat-sheet-portal`;
13
+ const DOCS_CHAT_SHEET_SCOPE_ID = `${DOCS_CHAT_USER_KEY}-docs-dataset-chat`;
10
14
 
11
15
  const DOCS_SAMPLE_SLASH_ITEMS: SlashCommandItem[] = [
12
16
  {
@@ -22,24 +26,29 @@ export default function ChatSheetPage() {
22
26
  <AppPageHeader
23
27
  breadcrumbs={[{ label: 'Chat' }, { label: 'Chat sheet' }]}
24
28
  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."
29
+ subheader={`Same integration as the app dataset page: ChatSheet in header actions with dataset-scoped presets for "${DOCS_SAMPLE_DATASET_NAME}". Panel portaled into the shell chat slot.`}
26
30
  actions={
27
31
  <DocsHeaderActions
28
32
  scopeId={DOCS_CHAT_SHEET_SCOPE_ID}
29
33
  slashCommandItems={DOCS_SAMPLE_SLASH_ITEMS}
30
- triggerLabel={
31
- <>
32
- <MessageSquare size={20} />
33
- &nbsp;AI Assistant
34
- </>
35
- }
34
+ presets={DOCS_DATASET_PRESETS}
35
+ triggerLabel={<MessageSquare size={20} />}
36
+ triggerAriaLabel="AI Assistant"
37
+ emptyState={{
38
+ title: 'Start a conversation',
39
+ }}
36
40
  />
37
41
  }
38
42
  />
39
43
  <PageContentSection>
40
44
  <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).
45
+ Open <strong>AI Assistant</strong> in the header. Presets render below
46
+ the empty state (fixed layout) and inline after the assistant reply,
47
+ same as{' '}
48
+ <code style={{ fontSize: 13 }}>
49
+ presets=&#123;datasetPresets&#125;
50
+ </code>{' '}
51
+ on the app dataset page.
43
52
  </p>
44
53
  <h3 style={{ marginBottom: 8, fontSize: 14, fontWeight: 600 }}>
45
54
  Regression checklist
@@ -47,6 +56,10 @@ export default function ChatSheetPage() {
47
56
  <ul
48
57
  style={{ margin: 0, paddingLeft: 20, fontSize: 14, lineHeight: 1.6 }}
49
58
  >
59
+ <li>
60
+ Empty chat → three outline preset chips with dataset-scoped labels
61
+ </li>
62
+ <li>Preset click → sends preset text, chip hides for that session</li>
50
63
  <li>Plain Enter with text → submit and clear composer</li>
51
64
  <li>
52
65
  Plain Enter must not create empty lines with duplicated placeholders
@@ -37,6 +37,28 @@ describe('requiredShellWidthForSidebars', () => {
37
37
  }),
38
38
  ).toBe(SIDEBAR_WIDTH_MIN_PX + CENTRAL_AREA_MIN_PX + CHAT_WIDTH_MIN_PX);
39
39
  });
40
+
41
+ it('uses panel mins for sub-min stored widths when both panels are open', () => {
42
+ expect(
43
+ requiredShellWidthForSidebars({
44
+ mainSidebarOpen: true,
45
+ chatPanelOpen: true,
46
+ sidebarWidthPx: 300,
47
+ chatWidthPx: 37,
48
+ }),
49
+ ).toBe(300 + CENTRAL_AREA_MIN_PX + CHAT_WIDTH_MIN_PX);
50
+ });
51
+
52
+ it('keeps actual chat width when only chat is open', () => {
53
+ expect(
54
+ requiredShellWidthForSidebars({
55
+ mainSidebarOpen: false,
56
+ chatPanelOpen: true,
57
+ sidebarWidthPx: 300,
58
+ chatWidthPx: 337,
59
+ }),
60
+ ).toBe(CENTRAL_AREA_MIN_PX + 337);
61
+ });
40
62
  });
41
63
 
42
64
  describe('shellFitsSidebarsLayout', () => {
@@ -62,6 +84,17 @@ describe('shellFitsSidebarsLayout', () => {
62
84
  ).toBe(false);
63
85
  });
64
86
 
87
+ it('returns false when sub-min chat width would falsely fit at shell edge', () => {
88
+ expect(
89
+ shellFitsSidebarsLayout(1137, {
90
+ mainSidebarOpen: true,
91
+ chatPanelOpen: true,
92
+ sidebarWidthPx: 300,
93
+ chatWidthPx: 37,
94
+ }),
95
+ ).toBe(false);
96
+ });
97
+
65
98
  it('returns true for unknown shell width', () => {
66
99
  expect(
67
100
  shellFitsSidebarsLayout(0, {
@@ -86,15 +86,40 @@ export type SidebarsLayoutWidths = {
86
86
  chatWidthPx: number;
87
87
  };
88
88
 
89
+ /**
90
+ * Stored width can fall below panel min when the shell was narrow (clamp cap < min).
91
+ * For dual-panel fit checks, treat open panels as at least their min width.
92
+ */
93
+ function effectiveOpenPanelWidthPx(
94
+ px: number,
95
+ minPx: number,
96
+ bothPanelsOpen: boolean,
97
+ ): number {
98
+ if (bothPanelsOpen) {
99
+ return Math.max(px, minPx);
100
+ }
101
+ return px > 0 ? px : minPx;
102
+ }
103
+
89
104
  /** Minimum shell width when nav and/or chat panels are open (main column at least `CENTRAL_AREA_MIN_PX`). */
90
105
  export function requiredShellWidthForSidebars(
91
106
  widths: SidebarsLayoutWidths,
92
107
  ): number {
108
+ const bothPanelsOpen =
109
+ widths.mainSidebarOpen && widths.chatPanelOpen;
93
110
  const sidebarW = widths.mainSidebarOpen
94
- ? widths.sidebarWidthPx || SIDEBAR_WIDTH_MIN_PX
111
+ ? effectiveOpenPanelWidthPx(
112
+ widths.sidebarWidthPx,
113
+ SIDEBAR_WIDTH_MIN_PX,
114
+ bothPanelsOpen,
115
+ )
95
116
  : 0;
96
117
  const chatW = widths.chatPanelOpen
97
- ? widths.chatWidthPx || CHAT_WIDTH_MIN_PX
118
+ ? effectiveOpenPanelWidthPx(
119
+ widths.chatWidthPx,
120
+ CHAT_WIDTH_MIN_PX,
121
+ bothPanelsOpen,
122
+ )
98
123
  : 0;
99
124
  return sidebarW + CENTRAL_AREA_MIN_PX + chatW;
100
125
  }