blixify-ui-web-mcp 0.1.53 → 0.1.55

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.
@@ -4,13 +4,13 @@
4
4
 
5
5
  <hr>
6
6
 
7
- A floating chat button fixed to the viewport. Clicking it toggles the `ChatBase` panel open/closed with an animated transition. On mobile (< 640px) the chat panel expands to full screen when open.
7
+ A floating chat button fixed to the viewport. Clicking it toggles the `ChatBase` panel open/closed with an animated transition. On mobile (< 640px) the chat panel expands to full screen by default. Enable `resizableHeight` to open a full-width, bottom-aligned panel with a touch-friendly height handle instead. Desktop keeps the floating panel.
8
8
 
9
- The button can be dragged out of the way on desktop and touch, and where it is dropped is kept in `localStorage` so it stays there on the next visit. A short drag is still treated as a click, so tapping always opens the chat. A saved position is clamped back into view when it is restored on a smaller screen or the window is resized under it, and dragging is disabled while the panel is full screen on mobile.
9
+ The button can be dragged out of the way on desktop and touch, and where it is dropped is kept in `localStorage` so it stays there on the next visit. A short drag is still treated as a click, so tapping always opens the chat. A saved position is clamped back into view when it is restored on a smaller screen or the window is resized under it, and button dragging is disabled while the chat is open on mobile.
10
10
 
11
11
  With `resizableWidth`, an open panel gains a grab handle on its left edge — dragging it left or right resizes the panel to any width. The only bounds are physical: it cannot be dragged wider than the viewport, nor narrower than 280px, below which the header, messages and input stop being usable. The chosen width is kept in `localStorage`, re-clamped to the viewport on window resize, and the handle is hidden while the panel is full screen on mobile.
12
12
 
13
- `resizableHeight` works the same way on the vertical axis: an open panel gains a grab handle on its top edge, and dragging it up or down resizes the panel to any height. Because the panel grows upward from the bottom-right anchor, the cap is the distance from its bottom edge to the top of the viewport, and the floor keeps the header and input row usable. The default height is three quarters of the viewport (matching the panel's `sm:h-[75vh]`). Width and height are independent — turn on either or both. The chosen height is kept in `localStorage` and re-clamped on window resize.
13
+ `resizableHeight` works the same way on the vertical axis: an open panel gains a grab handle on its top edge, and dragging it up or down resizes the panel to any height. Because the panel grows upward from the bottom-right anchor, the cap is the distance from its bottom edge to the top of the viewport, and the floor keeps the header and input row usable. The default height is three quarters of the viewport (matching the panel's `sm:h-[75vh]`). Width and height are independent — turn on either or both. The chosen height is kept in `localStorage` and re-clamped on window resize. On mobile, this enables a bottom-aligned panel with a larger top handle; see Mobile height resizing below.
14
14
 
15
15
  | Prop | Type | Default | Description |
16
16
  | -------------------- | --------- | -------------------------------- | ------------------------------------------------------------------------------------------- |
@@ -66,6 +66,36 @@ return (
66
66
  );
67
67
  ```
68
68
 
69
+ ##### Mobile height resizing
70
+
71
+ ```tsx
72
+ <ChatAnchor
73
+ {...chatProps}
74
+ resizableHeight
75
+ open={open}
76
+ onClick={() => setOpen(!open)}
77
+ onClose={() => setOpen(false)}
78
+ />
79
+ ```
80
+
81
+ On mobile, the chat is pinned to the bottom at full width. Drag the top handle up to extend it to the viewport height, or down to shrink it to 50% of the current viewport height. The initial height is 75% of the viewport unless `defaultHeight` or a saved height overrides it. The same `heightStorageKey` stores height on desktop and mobile, and restored heights are clamped to the available screen. Arrow Up/Down on the focused handle also adjusts height.
82
+
83
+ Your `headerElement` renders beneath the handle. Messages scroll above the composer. The chat close button calls `onClose`, falling back to `onClick`; if `hideAdditionalAction` hides that button, a close button appears beside the handle. The page remains accessible above the panel. Mobile sizing ignores `positionOffset`, saved button positions, `sizeClassName`, and `disableMobileResize` while open; the closed button keeps its saved position. Width resizing stays desktop-only. Without `resizableHeight`, mobile keeps its fullscreen layout. No `mobileView` prop or Drawer is needed.
84
+
85
+ ##### Keyboard and safe areas
86
+
87
+ On mobile, opening the keyboard temporarily fits the chat to the visible viewport and disables the outer height resize handle. The preferred height is preserved and restored when the keyboard closes; keyboard adjustments are never saved to `heightStorageKey`. If a keyboard interrupts an unfinished height drag, that drag is cancelled. Message scrolling and the composer remain available.
88
+
89
+ Browsers detect the keyboard from input focus within the chat and a substantial viewport height reduction. Pinch zoom and small browser toolbar changes do not activate this detection. `visualViewport` resize/scroll events position the panel above an overlay keyboard; window resize events also support WebViews that resize their layout viewport. Native hosts can override the detection:
90
+
91
+ ```tsx
92
+ <ChatAnchor {...chatProps} resizableHeight keyboardOpen={keyboardVisible} />
93
+ ```
94
+
95
+ Omit `keyboardOpen` for automatic detection. Supply `true`/`false` from the app's Capacitor keyboard events when available. This prop supplies visibility, not keyboard dimensions: the host must still configure its WebView to resize or expose a reduced visual viewport when the keyboard opens. No Capacitor plugin is required by ui-web.
96
+
97
+ The mobile panel respects `--safe-area-inset-*` CSS variables, falling back to `env(safe-area-inset-*, 0px)`. Bottom padding is omitted while the keyboard is open; top spacing protects the drag handle and header when the panel reaches the top of the visible viewport. Provide `viewport-fit=cover` and the relevant safe-area values in the host app. The 50–100% range applies to the keyboard-closed viewport; fitting the visible space takes priority while typing.
98
+
69
99
  ##### Required props
70
100
 
71
101
  | Name | Type | Description |
@@ -80,6 +110,7 @@ return (
80
110
 
81
111
  | Name | Type | Default | Description |
82
112
  | ---------------- | ----------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------- |
113
+ | `keyboardOpen` | `boolean` | Automatic detection | Optional native keyboard visibility override. Temporarily fits mobile chat and disables outer height resizing while open. |
83
114
  | `positionOffset` | `string` | `"bottom-[88px] right-4 md:bottom-6 md:right-6"` | Tailwind classes that control the fixed position of the anchor. On phones the default rests on the top edge of a bottom bar such as DataTemplate's Quick find, clear of its filter button. Ignored on mobile when open. |
84
115
  | `greetingCard` | `boolean` | `false` | When `true`, shows a greeting bubble next to the anchor button while the chat is closed. |
85
116
  | `greetingText` | `string` | `"Welcome! How can I help you today?"` | Text displayed inside the greeting card bubble. |
@@ -198,3 +198,73 @@ export const Resizable = () => {
198
198
  </div>
199
199
  );
200
200
  };
201
+
202
+ export const CustomHeader = () => {
203
+ const [open, setOpen] = useState(false);
204
+ const [message, setMessage] = useState("");
205
+ const currentUser = { id: "1", name: "Guest" };
206
+ const botUser = { id: "2", name: "Support" };
207
+ const [conversation, setConversation] = useState<Conversation>({
208
+ id: "custom-header",
209
+ members: [currentUser, botUser],
210
+ messageList: [
211
+ "Hi! Welcome to Blixify support. How can we help today?",
212
+ "Can I customize the chat to match my brand?",
213
+ "Absolutely. Your header can include your logo, team details, and availability.",
214
+ "Will the same header work on mobile?",
215
+ "Yes! Open this example at a mobile width to see how it adapts.",
216
+ ].map((messageBody, index) => ({
217
+ sender: index % 2 ? currentUser : botUser,
218
+ messageBody,
219
+ createdAt: new Date("2026-10-16T00:00:00Z"),
220
+ })),
221
+ });
222
+ const handleToggleChat = () => setOpen((previous) => !previous);
223
+ const handleSendMessage = (nextConversation: Conversation) => {
224
+ setConversation(nextConversation);
225
+ setMessage("");
226
+ };
227
+
228
+ return (
229
+ <div className="min-h-screen p-6 bg-gray-50">
230
+ <p>
231
+ Open chat to see a custom branded header with a team avatar, availability,
232
+ and response time. The same header adapts to desktop and mobile.
233
+ </p>
234
+ <ChatAnchorImpl
235
+ resizableHeight
236
+ headerElement={
237
+ <div className="bg-gradient-to-br from-indigo-900 to-indigo-700 text-white">
238
+ <div className="flex items-center gap-3 py-5 pl-4 pr-24">
239
+ <div className="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-white/15 text-lg font-semibold ring-1 ring-white/20">
240
+ B
241
+ <span className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full bg-indigo-400 ring-2 ring-indigo-900" />
242
+ </div>
243
+ <div className="min-w-0">
244
+ <p className="text-xs font-medium uppercase tracking-wider text-indigo-200">
245
+ Customer care
246
+ </p>
247
+ <h2 className="text-base font-semibold">Blixify support</h2>
248
+ </div>
249
+ </div>
250
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 border-t border-white/15 bg-black/10 px-4 py-3 text-xs">
251
+ <span className="flex items-center gap-1.5 font-medium">
252
+ <span className="h-1.5 w-1.5 rounded-full bg-indigo-300" />
253
+ Online now
254
+ </span>
255
+ <span className="text-indigo-100">Usually replies in a few minutes</span>
256
+ </div>
257
+ </div>
258
+ }
259
+ open={open}
260
+ value={message}
261
+ currentUser={currentUser}
262
+ conversation={conversation}
263
+ onClick={handleToggleChat}
264
+ onClose={() => setOpen(false)}
265
+ onChangeMessage={(event) => setMessage(event.target.value)}
266
+ onSendMessage={handleSendMessage}
267
+ />
268
+ </div>
269
+ );
270
+ };
@@ -751,3 +751,85 @@ export const FlatTable = () => {
751
751
  </StorySection>
752
752
  );
753
753
  };
754
+
755
+ // INFO: a row that opens on click, with a link, a checkbox and a button placed
756
+ // in its cells — the e2e checks which of them open the row
757
+ export const RowLinks = () => {
758
+ const [opened, setOpened] = useState<string[]>([]);
759
+ const [ticked, setTicked] = useState(false);
760
+ const rows = [
761
+ {
762
+ id: "row-1",
763
+ name: "Alarm log",
764
+ download: (
765
+ <a href="#downloaded" data-testid="row-download-link">
766
+ Download
767
+ </a>
768
+ ),
769
+ ack: (
770
+ <input
771
+ type="checkbox"
772
+ data-testid="row-checkbox"
773
+ checked={ticked}
774
+ onChange={(e) => setTicked(e.target.checked)}
775
+ />
776
+ ),
777
+ action: (
778
+ <IconButton
779
+ testId="row-icon-button"
780
+ size="small"
781
+ icon={<span className="text-xs">Go</span>}
782
+ onClick={() => {}}
783
+ />
784
+ ),
785
+ },
786
+ ];
787
+ return (
788
+ <StorySection
789
+ title="Links and fields in a row"
790
+ description="A link or form field in a cell does its own action and does not open the row. A button still opens the row unless it stops the click."
791
+ >
792
+ <NewTableImpl
793
+ columns={[
794
+ { key: "name", title: "Name" },
795
+ { key: "download", title: "" },
796
+ { key: "ack", title: "" },
797
+ { key: "action", title: "" },
798
+ ]}
799
+ data={rows}
800
+ size={rows.length}
801
+ limit={10}
802
+ paginationType="page"
803
+ handleUpdatePageIndex={() => {}}
804
+ onClickRow={(id) => setOpened((previous) => [...previous, id])}
805
+ />
806
+ <pre data-testid="row-opened" className="mt-3 text-xs">
807
+ {JSON.stringify(opened)}
808
+ </pre>
809
+ <NewTableImpl
810
+ columns={[
811
+ { key: "name", title: "Name" },
812
+ { key: "preview", title: "" },
813
+ ]}
814
+ data={[
815
+ {
816
+ id: "row-2",
817
+ name: "Open in place",
818
+ preview: (
819
+ <button type="button" data-testid="row-link-button">
820
+ Preview
821
+ </button>
822
+ ),
823
+ },
824
+ ]}
825
+ size={1}
826
+ limit={10}
827
+ paginationType="page"
828
+ handleUpdatePageIndex={() => {}}
829
+ newTabUrl="#record-"
830
+ linkType="default"
831
+ onClickRow={(id) => setOpened((previous) => [...previous, id])}
832
+ />
833
+ </StorySection>
834
+ );
835
+ };
@@ -165,6 +165,7 @@ Below `lg` the rail moves into the mobile drawer as a row of icons above the sid
165
165
  | `compactMode` | `boolean` | `false` | Tighter rows, smaller icons and a smaller expand chevron, keeping every label |
166
166
  | `searchable` | `boolean` | `false` | Shows a filter box. Matches are listed flat, each with the path it sits under (e.g. `Sales › Reports`) |
167
167
  | `searchAction` | `ReactElement` | — | A small control inside the right end of the filter box, such as a filter button opening the app's own pop-up. Shown only with `searchable` |
168
+ | `keepSearchAncestors` | `boolean` | `false` | With `searchable`, keeps each match under its parents (a workspace, a folder page), shown open, instead of the flat list. Rows that neither match nor hold a match are left out |
168
169
  | `persistKey` | `string` | — | localStorage key that remembers open groups and pages. A new key loads that key's stored state. |
169
170
  | `defaultExpandedIds` | `string[]` | — | Groups (by `groupId`) and pages (by `id`) open when nothing is stored for them |
170
171
 
@@ -584,6 +584,27 @@ export const LeftNav = () => {
584
584
  </div>
585
585
  </StorySection>
586
586
 
587
+ <StorySection
588
+ title="Search Keeping Parents"
589
+ description="keepSearchAncestors keeps each match where it sits while the filter has text — under its parents, shown open — so a page found in a nested folder still shows which branch it belongs to. Type 'vol' and the volumes stay under the rows above them. Rows that neither match nor hold a match are left out, and clearing the box brings back the tree as it was."
590
+ >
591
+ <div className="w-full h-96 relative overflow-hidden border rounded-lg">
592
+ <LeftNavImpl
593
+ selectedId={selectedId}
594
+ navigation={manyNavigation}
595
+ {...handleMobileMenu("story-7b")}
596
+ searchable={true}
597
+ keepSearchAncestors={true}
598
+ compactMode={true}
599
+ searchPlaceholder="Filter pages"
600
+ handleBarsOnClick={(id) => setSelectedId(id)}
601
+ renderDashboardLogo={() => renderLogo()}
602
+ renderDashboardContent={renderContent}
603
+ renderDashboardHeaderContent={renderHeader}
604
+ />
605
+ </div>
606
+ </StorySection>
607
+
587
608
  <StorySection
588
609
  title="Search Action"
589
610
  description="searchAction puts a small control inside the right end of the filter box, so narrowing the nav costs no row of its own. Here a funnel button toggles a filter the app owns — top-level pages only — and shows a dot while it is on. LeftNav only draws the control; what it opens and filters is up to the app."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blixify-ui-web-mcp",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "license": "MIT",
5
5
  "main": "lib/mcp/server.js",
6
6
  "bin": {