@ahmedramez-org/nexus-chat 1.0.0-beta.23 → 1.0.0-beta.24

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/README.md CHANGED
@@ -17,7 +17,7 @@ build itself is not in this package: Copilot serves it, and each GitHub Release
17
17
  ```
18
18
 
19
19
  Incorta serves this script next to `embed.html`; it is not in this package and needs no build. It
20
- draws the Incorta Intelligence launcher at the bottom right and opens the embedded chat (below)
20
+ draws the Incorta Intelligence launcher at the bottom right (bottom left on a `dir="rtl"` page) and opens the embedded chat (below)
21
21
  floating, docked or full screen, on the user's Incorta session, so the page holds no token. It is
22
22
  one classic script with no imports, so a page on any origin loads it without CORS.
23
23
 
@@ -42,7 +42,8 @@ import { NexusChat } from '@incorta/nexus-chat/react';
42
42
 
43
43
  It draws its own window: an Incorta Intelligence launcher at the bottom right opens the chat
44
44
  floating (a card lifted off the page, opening out of the launcher, which stays under it to hide
45
- it) or docked flush on the right (the page makes room). The chat's own top bar switches between
45
+ it) or docked flush on the right (the page makes room). On a `dir="rtl"` page the launcher and
46
+ window sit on the left, following a live `dir` change. The chat's own top bar switches between
46
47
  the windows (Float, Dock to the side, Enter full screen: every allowed one but the current) and
47
48
  closes it back to the launcher; nothing sits above the chat. `modes` limits the windows (default
48
49
  all three, in that order) and `defaultMode` opens one on load. Below 768px wide the chat covers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmedramez-org/nexus-chat",
3
- "version": "1.0.0-beta.23",
3
+ "version": "1.0.0-beta.24",
4
4
  "description": "NexusChat for any app: a connector and React component that load the prebuilt chat from IncortaCopilot (or a pinned copy), and the host contract types and checks.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/react.d.ts CHANGED
@@ -75,7 +75,7 @@ export type NexusChatMode = NexusChatWindowMode;
75
75
  /**
76
76
  * The chat in an <iframe> served by Incorta, on the user's own Incorta
77
77
  * session (the app never holds a token). It draws its window: a launcher
78
- * icon that opens the chat floating, docked on the right, or fullscreen.
78
+ * icon that opens the chat floating, docked on the right (left on an RTL page), or fullscreen.
79
79
  */
80
80
  export interface EmbeddedNexusChatProps {
81
81
  /** Incorta's chat page: `<incorta>/copilot/nexus-chat/embed.html`. */
package/window.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // The chat's window, without a framework: a launcher icon that opens a panel
2
- // floating, docked on the right (the page makes room) or full screen, and the
2
+ // floating, docked at the page's end edge (the page makes room; left in RTL) or full screen, and the
3
3
  // embedded chat's side of the iframe protocol. <NexusChat> (react.js) and the
4
4
  // one-line script (widget.js) both draw the window with this, so they behave
5
5
  // the same.
@@ -7,7 +7,7 @@ import { ICON, PALETTE, reducedMotion, showNexusChatSkeleton } from './index.js'
7
7
 
8
8
  export const MODES = ['floating', 'docked', 'fullscreen'];
9
9
  const PANEL_WIDTH = 400;
10
- /** The floating window opens out of the launcher, right edges aligned, 12px above it. */
10
+ /** The floating window opens out of the launcher, end edges aligned, 12px above it. */
11
11
  const FLOATING = { width: 380, height: 620, edge: 24, launcher: 52, gap: 12 };
12
12
  /** Below this width a floating or docked chat would crush the app, so it covers it. */
13
13
  const NARROW_VIEWPORT = 768;
@@ -32,45 +32,48 @@ const { edge, launcher: LAUNCHER_SIZE, gap } = FLOATING;
32
32
  /** Every placement property any mode sets, cleared before the next mode's. */
33
33
  const PLACED = [
34
34
  'top',
35
- 'right',
35
+ 'insetInlineEnd',
36
36
  'bottom',
37
- 'left',
37
+ 'insetInlineStart',
38
38
  'inset',
39
39
  'width',
40
40
  'height',
41
41
  'border',
42
- 'borderLeft',
42
+ 'borderInlineStart',
43
43
  'borderRadius',
44
44
  'boxShadow',
45
45
  'transformOrigin',
46
46
  ];
47
47
 
48
48
  const isNarrow = () => typeof window !== 'undefined' && window.innerWidth < NARROW_VIEWPORT;
49
+ /** Placement follows the page's `dir`: logical properties, plus this for what has none. */
50
+ const isRtl = () =>
51
+ typeof document !== 'undefined' && getComputedStyle(document.documentElement).direction === 'rtl';
49
52
  const tone = (theme) => (theme === 'dark' ? 'dark' : 'light');
50
53
 
51
54
  function placement(mode, theme) {
52
55
  const palette = PALETTE[tone(theme)];
53
56
  if (mode === 'docked') {
54
- // Flush against the page's right edge, full height: only a hairline between them.
57
+ // Flush against the page's end edge (right, or left in RTL), full height: only a hairline between them.
55
58
  return {
56
59
  top: '0',
57
- right: '0',
60
+ insetInlineEnd: '0',
58
61
  bottom: '0',
59
62
  width: `${PANEL_WIDTH}px`,
60
- borderLeft: `1px solid ${palette.line}`,
63
+ borderInlineStart: `1px solid ${palette.line}`,
61
64
  };
62
65
  }
63
66
  if (mode === 'fullscreen') return { inset: '0' };
64
67
  // A card lifted off the page, opening out of the launcher below it.
65
68
  return {
66
- right: `${edge}px`,
69
+ insetInlineEnd: `${edge}px`,
67
70
  bottom: `${edge + LAUNCHER_SIZE + gap}px`,
68
71
  width: `min(${FLOATING.width}px, calc(100vw - ${2 * edge}px))`,
69
72
  height: `min(${FLOATING.height}px, calc(100vh - ${2 * edge + LAUNCHER_SIZE + gap}px))`,
70
73
  border: `1px solid ${palette.line}`,
71
74
  borderRadius: '16px',
72
75
  boxShadow: LIFT[tone(theme)],
73
- transformOrigin: 'bottom right',
76
+ transformOrigin: `bottom ${isRtl() ? 'left' : 'right'}`,
74
77
  };
75
78
  }
76
79
 
@@ -129,12 +132,12 @@ export function createChatWindow({ into, panel, modes = MODES, defaultMode, them
129
132
  close.title = 'Close';
130
133
  close.textContent = '×';
131
134
  close.style.cssText =
132
- 'position:absolute;top:12px;right:14px;z-index:1;width:36px;height:36px;border:0;border-radius:8px;background:transparent;color:inherit;font:20px/1 system-ui,sans-serif;cursor:pointer';
135
+ 'position:absolute;top:12px;inset-inline-end:14px;z-index:1;width:36px;height:36px;border:0;border-radius:8px;background:transparent;color:inherit;font:20px/1 system-ui,sans-serif;cursor:pointer';
133
136
  close.onclick = () => show('minimized');
134
137
 
135
138
  const button = document.createElement('button');
136
139
  button.type = 'button';
137
- button.style.cssText = `position:fixed;right:${edge}px;bottom:${edge}px;z-index:50;box-sizing:border-box;width:${LAUNCHER_SIZE}px;height:${LAUNCHER_SIZE}px;padding:0;border-radius:50%;place-items:center;cursor:pointer`;
140
+ button.style.cssText = `position:fixed;inset-inline-end:${edge}px;bottom:${edge}px;z-index:50;box-sizing:border-box;width:${LAUNCHER_SIZE}px;height:${LAUNCHER_SIZE}px;padding:0;border-radius:50%;place-items:center;cursor:pointer`;
138
141
  button.onclick = () =>
139
142
  show(view === 'floating' && !narrow ? 'minimized' : (last ?? 'fullscreen'));
140
143
  into.appendChild(button);
@@ -175,10 +178,10 @@ export function createChatWindow({ into, panel, modes = MODES, defaultMode, them
175
178
  // A docked panel takes its width from the page instead of covering it.
176
179
  const root = document.documentElement;
177
180
  if (now === 'docked' && paddedFrom === undefined) {
178
- paddedFrom = root.style.paddingRight;
179
- root.style.paddingRight = `${PANEL_WIDTH}px`;
181
+ paddedFrom = root.style.paddingInlineEnd;
182
+ root.style.paddingInlineEnd = `${PANEL_WIDTH}px`;
180
183
  } else if (now !== 'docked' && paddedFrom !== undefined) {
181
- root.style.paddingRight = paddedFrom;
184
+ root.style.paddingInlineEnd = paddedFrom;
182
185
  paddedFrom = undefined;
183
186
  }
184
187
  shownBefore = now;
@@ -257,7 +260,7 @@ export function createChatWindow({ into, panel, modes = MODES, defaultMode, them
257
260
  destroy() {
258
261
  destroyed = true;
259
262
  window.removeEventListener('resize', onResize);
260
- if (paddedFrom !== undefined) document.documentElement.style.paddingRight = paddedFrom;
263
+ if (paddedFrom !== undefined) document.documentElement.style.paddingInlineEnd = paddedFrom;
261
264
  paddedFrom = undefined;
262
265
  button.remove();
263
266
  close.remove();