tycho-components 0.41.0 → 0.41.2

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.
@@ -33,6 +33,10 @@ export declare const CommentsTexts: {
33
33
  "tab.write": string;
34
34
  "tab.preview": string;
35
35
  "placeholder.preview.empty": string;
36
+ "menu.undock": string;
37
+ "menu.dock.left": string;
38
+ "menu.dock.bottom": string;
39
+ "menu.dock.right": string;
36
40
  };
37
41
  "pt-BR": {
38
42
  "label.title.comments": string;
@@ -68,6 +72,10 @@ export declare const CommentsTexts: {
68
72
  "tab.write": string;
69
73
  "tab.preview": string;
70
74
  "placeholder.preview.empty": string;
75
+ "menu.undock": string;
76
+ "menu.dock.left": string;
77
+ "menu.dock.bottom": string;
78
+ "menu.dock.right": string;
71
79
  };
72
80
  it: {
73
81
  "label.title.comments": string;
@@ -103,5 +111,9 @@ export declare const CommentsTexts: {
103
111
  "tab.write": string;
104
112
  "tab.preview": string;
105
113
  "placeholder.preview.empty": string;
114
+ "menu.undock": string;
115
+ "menu.dock.left": string;
116
+ "menu.dock.bottom": string;
117
+ "menu.dock.right": string;
106
118
  };
107
119
  };
@@ -33,6 +33,10 @@ export const CommentsTexts = {
33
33
  "tab.write": "Write",
34
34
  "tab.preview": "Preview",
35
35
  "placeholder.preview.empty": "Nothing to preview",
36
+ "menu.undock": "Undock into separate window",
37
+ "menu.dock.left": "Dock to left",
38
+ "menu.dock.bottom": "Dock to bottom",
39
+ "menu.dock.right": "Dock to right",
36
40
  },
37
41
  "pt-BR": {
38
42
  "label.title.comments": "Comentários",
@@ -68,6 +72,10 @@ export const CommentsTexts = {
68
72
  "tab.write": "Escrever",
69
73
  "tab.preview": "Visualizar",
70
74
  "placeholder.preview.empty": "Nada para visualizar",
75
+ "menu.undock": "Desencaixar em janela separada",
76
+ "menu.dock.left": "Encaixar à esquerda",
77
+ "menu.dock.bottom": "Encaixar embaixo",
78
+ "menu.dock.right": "Encaixar à direita",
71
79
  },
72
80
  it: {
73
81
  "label.title.comments": "Commenti",
@@ -103,5 +111,9 @@ export const CommentsTexts = {
103
111
  "tab.write": "Scrivi",
104
112
  "tab.preview": "Anteprima",
105
113
  "placeholder.preview.empty": "Niente da visualizzare",
114
+ "menu.undock": "Sgancia in una finestra separata",
115
+ "menu.dock.left": "Aggancia a sinistra",
116
+ "menu.dock.bottom": "Aggancia in basso",
117
+ "menu.dock.right": "Aggancia a destra",
106
118
  },
107
119
  };
@@ -0,0 +1,26 @@
1
+ export type DockPosition = 'left' | 'right' | 'bottom' | 'undocked';
2
+ export type DockedPosition = Exclude<DockPosition, 'undocked'>;
3
+ declare const CommentUtils: {
4
+ isDockPosition: (value: string | null) => value is DockPosition;
5
+ isDockedPosition: (value: DockPosition) => value is DockedPosition;
6
+ clampDrawerWidth: (width: number) => number;
7
+ clampDrawerHeight: (height: number) => number;
8
+ readStoredDrawerWidth: () => number;
9
+ readStoredDrawerHeight: () => number;
10
+ persistDrawerWidth: (width: number) => void;
11
+ persistDrawerHeight: (height: number) => void;
12
+ readStoredDockPosition: () => DockPosition;
13
+ persistDockPosition: (position: DockPosition) => void;
14
+ readStoredLastDocked: () => DockedPosition;
15
+ persistLastDocked: (position: DockedPosition) => void;
16
+ getUndockedPopupSize: () => {
17
+ width: number;
18
+ height: number;
19
+ };
20
+ copyStylesToPopup: (popup: Window) => void;
21
+ openCommentsPopup: () => {
22
+ popup: Window;
23
+ container: HTMLElement;
24
+ } | null;
25
+ };
26
+ export default CommentUtils;
@@ -0,0 +1,202 @@
1
+ const DRAWER_WIDTH_STORAGE_KEY = 'tycho-comments-drawer-width';
2
+ const DRAWER_HEIGHT_STORAGE_KEY = 'tycho-comments-drawer-height';
3
+ const DOCK_POSITION_STORAGE_KEY = 'tycho-comments-dock-position';
4
+ const LAST_DOCKED_STORAGE_KEY = 'tycho-comments-last-docked';
5
+ const DRAWER_MIN_WIDTH = 320;
6
+ const DRAWER_MIN_HEIGHT = 200;
7
+ const POPUP_NAME = 'tycho-comments';
8
+ const POPUP_MIN_WIDTH = 960;
9
+ const POPUP_MIN_HEIGHT = 720;
10
+ const isDockPosition = (value) => value === 'left' ||
11
+ value === 'right' ||
12
+ value === 'bottom' ||
13
+ value === 'undocked';
14
+ const isDockedPosition = (value) => value !== 'undocked';
15
+ const getDefaultDrawerWidth = () => {
16
+ if (typeof window === 'undefined')
17
+ return 480;
18
+ const { innerWidth } = window;
19
+ if (innerWidth <= 767)
20
+ return Math.round(innerWidth * 0.9);
21
+ if (innerWidth <= 768)
22
+ return Math.round(innerWidth * 0.6);
23
+ return Math.round(innerWidth * 0.4);
24
+ };
25
+ const getDefaultDrawerHeight = () => {
26
+ if (typeof window === 'undefined')
27
+ return 360;
28
+ return Math.round(window.innerHeight * 0.4);
29
+ };
30
+ const clampDrawerWidth = (width) => {
31
+ if (typeof window === 'undefined')
32
+ return width;
33
+ const maxWidth = Math.min(Math.round(window.innerWidth * 0.9), window.innerWidth - 48);
34
+ return Math.min(Math.max(width, DRAWER_MIN_WIDTH), maxWidth);
35
+ };
36
+ const clampDrawerHeight = (height) => {
37
+ if (typeof window === 'undefined')
38
+ return height;
39
+ const maxHeight = Math.min(Math.round(window.innerHeight * 0.9), window.innerHeight - 48);
40
+ return Math.min(Math.max(height, DRAWER_MIN_HEIGHT), maxHeight);
41
+ };
42
+ const readStoredDrawerWidth = () => {
43
+ try {
44
+ const stored = localStorage.getItem(DRAWER_WIDTH_STORAGE_KEY);
45
+ if (stored) {
46
+ const parsed = Number(stored);
47
+ if (!Number.isNaN(parsed) && parsed > 0) {
48
+ return clampDrawerWidth(parsed);
49
+ }
50
+ }
51
+ }
52
+ catch {
53
+ // ignore storage access errors
54
+ }
55
+ return clampDrawerWidth(getDefaultDrawerWidth());
56
+ };
57
+ const readStoredDrawerHeight = () => {
58
+ try {
59
+ const stored = localStorage.getItem(DRAWER_HEIGHT_STORAGE_KEY);
60
+ if (stored) {
61
+ const parsed = Number(stored);
62
+ if (!Number.isNaN(parsed) && parsed > 0) {
63
+ return clampDrawerHeight(parsed);
64
+ }
65
+ }
66
+ }
67
+ catch {
68
+ // ignore storage access errors
69
+ }
70
+ return clampDrawerHeight(getDefaultDrawerHeight());
71
+ };
72
+ const persistDrawerWidth = (width) => {
73
+ try {
74
+ localStorage.setItem(DRAWER_WIDTH_STORAGE_KEY, String(width));
75
+ }
76
+ catch {
77
+ // ignore storage access errors
78
+ }
79
+ };
80
+ const persistDrawerHeight = (height) => {
81
+ try {
82
+ localStorage.setItem(DRAWER_HEIGHT_STORAGE_KEY, String(height));
83
+ }
84
+ catch {
85
+ // ignore storage access errors
86
+ }
87
+ };
88
+ const readStoredDockPosition = () => {
89
+ try {
90
+ const stored = localStorage.getItem(DOCK_POSITION_STORAGE_KEY);
91
+ if (isDockPosition(stored)) {
92
+ return stored;
93
+ }
94
+ }
95
+ catch {
96
+ // ignore storage access errors
97
+ }
98
+ return 'right';
99
+ };
100
+ const persistDockPosition = (position) => {
101
+ try {
102
+ localStorage.setItem(DOCK_POSITION_STORAGE_KEY, position);
103
+ }
104
+ catch {
105
+ // ignore storage access errors
106
+ }
107
+ };
108
+ const readStoredLastDocked = () => {
109
+ try {
110
+ const stored = localStorage.getItem(LAST_DOCKED_STORAGE_KEY);
111
+ if (stored === 'left' || stored === 'right' || stored === 'bottom') {
112
+ return stored;
113
+ }
114
+ const dock = localStorage.getItem(DOCK_POSITION_STORAGE_KEY);
115
+ if (dock === 'left' || dock === 'right' || dock === 'bottom') {
116
+ return dock;
117
+ }
118
+ }
119
+ catch {
120
+ // ignore storage access errors
121
+ }
122
+ return 'right';
123
+ };
124
+ const persistLastDocked = (position) => {
125
+ try {
126
+ localStorage.setItem(LAST_DOCKED_STORAGE_KEY, position);
127
+ }
128
+ catch {
129
+ // ignore storage access errors
130
+ }
131
+ };
132
+ const getUndockedPopupSize = () => {
133
+ if (typeof window === 'undefined') {
134
+ return { width: POPUP_MIN_WIDTH, height: POPUP_MIN_HEIGHT };
135
+ }
136
+ const availWidth = window.screen.availWidth || window.outerWidth;
137
+ const availHeight = window.screen.availHeight || window.outerHeight;
138
+ const width = Math.min(Math.max(Math.round(availWidth * 0.75), POPUP_MIN_WIDTH), availWidth - 40);
139
+ const height = Math.min(Math.max(Math.round(availHeight * 0.85), POPUP_MIN_HEIGHT), availHeight - 40);
140
+ return { width, height };
141
+ };
142
+ const copyStylesToPopup = (popup) => {
143
+ const { document: popupDoc } = popup;
144
+ popupDoc.head.innerHTML = '';
145
+ popupDoc.title = document.title;
146
+ document.querySelectorAll('link[rel="stylesheet"], style').forEach((node) => {
147
+ popupDoc.head.appendChild(node.cloneNode(true));
148
+ });
149
+ popupDoc.body.className = document.body.className;
150
+ popupDoc.body.style.margin = '0';
151
+ popupDoc.body.style.height = '100%';
152
+ popupDoc.documentElement.style.height = '100%';
153
+ popupDoc.documentElement.style.overflow = 'hidden';
154
+ };
155
+ const openCommentsPopup = () => {
156
+ if (typeof window === 'undefined')
157
+ return null;
158
+ const { width, height } = getUndockedPopupSize();
159
+ const left = Math.round(window.screenX + (window.outerWidth - width) / 2);
160
+ const top = Math.round(window.screenY + (window.outerHeight - height) / 2);
161
+ const features = [
162
+ 'popup=yes',
163
+ `width=${width}`,
164
+ `height=${height}`,
165
+ `left=${left}`,
166
+ `top=${top}`,
167
+ ].join(',');
168
+ const popup = window.open('', POPUP_NAME, features);
169
+ if (!popup)
170
+ return null;
171
+ try {
172
+ popup.resizeTo(width, height);
173
+ popup.moveTo(left, top);
174
+ }
175
+ catch {
176
+ // some browsers block resize/move on already-open named windows
177
+ }
178
+ copyStylesToPopup(popup);
179
+ const container = popup.document.createElement('div');
180
+ container.id = 'tycho-comments-root';
181
+ container.style.height = '100%';
182
+ popup.document.body.appendChild(container);
183
+ return { popup, container };
184
+ };
185
+ const CommentUtils = {
186
+ isDockPosition,
187
+ isDockedPosition,
188
+ clampDrawerWidth,
189
+ clampDrawerHeight,
190
+ readStoredDrawerWidth,
191
+ readStoredDrawerHeight,
192
+ persistDrawerWidth,
193
+ persistDrawerHeight,
194
+ readStoredDockPosition,
195
+ persistDockPosition,
196
+ readStoredLastDocked,
197
+ persistLastDocked,
198
+ getUndockedPopupSize,
199
+ copyStylesToPopup,
200
+ openCommentsPopup,
201
+ };
202
+ export default CommentUtils;
@@ -1,48 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Drawer } from '@mui/material';
3
+ import cx from 'classnames';
3
4
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
5
+ import { createPortal } from 'react-dom';
4
6
  import { useTranslation } from 'react-i18next';
5
- import { Button, IconButton } from 'tycho-storybook';
7
+ import { Button, DropdownMenu, IconButton } from 'tycho-storybook';
6
8
  import AppLoading from '../../common/AppLoading';
7
9
  import AppPlaceholder from '../../common/AppPlaceholder';
8
10
  import SecurityUtils from '../../functions/SecurityUtils';
9
11
  import CommentAdd from './CommentAdd';
10
12
  import CommentService from './CommentService';
11
13
  import CommentThread from './CommentThread';
14
+ import CommentUtils from './CommentUtils';
12
15
  import './style.scss';
13
- const DRAWER_WIDTH_STORAGE_KEY = 'tycho-comments-drawer-width';
14
- const DRAWER_MIN_WIDTH = 320;
15
- const getDefaultDrawerWidth = () => {
16
- if (typeof window === 'undefined')
17
- return 480;
18
- const { innerWidth } = window;
19
- if (innerWidth <= 767)
20
- return Math.round(innerWidth * 0.9);
21
- if (innerWidth <= 768)
22
- return Math.round(innerWidth * 0.6);
23
- return Math.round(innerWidth * 0.4);
24
- };
25
- const clampDrawerWidth = (width) => {
26
- if (typeof window === 'undefined')
27
- return width;
28
- const maxWidth = Math.min(Math.round(window.innerWidth * 0.9), window.innerWidth - 48);
29
- return Math.min(Math.max(width, DRAWER_MIN_WIDTH), maxWidth);
30
- };
31
- const readStoredDrawerWidth = () => {
32
- try {
33
- const stored = localStorage.getItem(DRAWER_WIDTH_STORAGE_KEY);
34
- if (stored) {
35
- const parsed = Number(stored);
36
- if (!Number.isNaN(parsed) && parsed > 0) {
37
- return clampDrawerWidth(parsed);
38
- }
39
- }
40
- }
41
- catch {
42
- // ignore storage access errors
43
- }
44
- return clampDrawerWidth(getDefaultDrawerWidth());
45
- };
16
+ function CommentsPanel({ dockPosition, showResizeHandle, onResizePointerDown, children, }) {
17
+ const isHorizontal = dockPosition === 'bottom';
18
+ return (_jsxs("div", { className: cx('comments-container', {
19
+ 'dock-left': dockPosition === 'left',
20
+ 'dock-right': dockPosition === 'right',
21
+ 'dock-bottom': dockPosition === 'bottom',
22
+ 'dock-undocked': dockPosition === 'undocked',
23
+ }), id: "comment-top", children: [showResizeHandle && (_jsx("div", { className: "comments-resize-handle", onPointerDown: onResizePointerDown, role: "separator", "aria-orientation": isHorizontal ? 'horizontal' : 'vertical', "aria-label": "Resize comments panel" })), children] }));
24
+ }
25
+ function CommentsHeader({ hasEditAccess, onToggleAdd, onClose, dockMenuOptions, }) {
26
+ const { t } = useTranslation('comments');
27
+ return (_jsxs("div", { className: "header", children: [_jsx(IconButton, { name: "close", size: "small", mode: "ghost", onClick: onClose }), _jsx("span", { className: "title", children: t('label.title.comments') }), _jsxs("div", { className: "actions", children: [hasEditAccess && (_jsx(Button, { text: t('button.add'), icon: "add", mode: "outlined", size: "small", className: "edit-button", onClick: onToggleAdd })), _jsx(DropdownMenu, { list: dockMenuOptions })] })] }));
28
+ }
46
29
  export default function Comments({ uid, keywords, references, mode, onClose, onChange, initialOpenAdd = false, }) {
47
30
  const { t } = useTranslation('comments');
48
31
  const [openAddComment, setOpenAddComment] = useState(initialOpenAdd);
@@ -50,8 +33,20 @@ export default function Comments({ uid, keywords, references, mode, onClose, onC
50
33
  const [comments, setComments] = useState();
51
34
  const [users, setUsers] = useState([]);
52
35
  const [reply, setReply] = useState();
53
- const [drawerWidth, setDrawerWidth] = useState(readStoredDrawerWidth);
36
+ const [drawerWidth, setDrawerWidth] = useState(CommentUtils.readStoredDrawerWidth);
37
+ const [drawerHeight, setDrawerHeight] = useState(CommentUtils.readStoredDrawerHeight);
38
+ const [dockPosition, setDockPosition] = useState(CommentUtils.readStoredDockPosition);
39
+ const [externalContainer, setExternalContainer] = useState(null);
54
40
  const commentAddRef = useRef(null);
41
+ const popupRef = useRef(null);
42
+ const popupPollRef = useRef(null);
43
+ const ignorePopupCloseRef = useRef(false);
44
+ const popupCloseHandledRef = useRef(false);
45
+ const restoredUndockRef = useRef(false);
46
+ const lastDockedRef = useRef(null);
47
+ if (lastDockedRef.current === null) {
48
+ lastDockedRef.current = CommentUtils.readStoredLastDocked();
49
+ }
55
50
  const load = async () => {
56
51
  try {
57
52
  const [commentsResponse, usersResponse] = await Promise.all([
@@ -63,15 +58,134 @@ export default function Comments({ uid, keywords, references, mode, onClose, onC
63
58
  }
64
59
  catch (error) {
65
60
  console.error('Error loading comments or users', error);
66
- // optionally handle specific error state here
67
61
  }
68
62
  };
69
63
  const hasEditAccess = useMemo(() => {
70
64
  return SecurityUtils.hasAccess(uid, ['ADMIN', 'EDITOR'], mode);
71
65
  }, [uid, mode]);
66
+ const clearPopupPoll = useCallback(() => {
67
+ if (popupPollRef.current != null) {
68
+ window.clearInterval(popupPollRef.current);
69
+ popupPollRef.current = null;
70
+ }
71
+ }, []);
72
+ const handlePopupClosed = useCallback(() => {
73
+ clearPopupPoll();
74
+ popupRef.current = null;
75
+ setExternalContainer(null);
76
+ if (ignorePopupCloseRef.current) {
77
+ ignorePopupCloseRef.current = false;
78
+ popupCloseHandledRef.current = true;
79
+ return;
80
+ }
81
+ if (popupCloseHandledRef.current)
82
+ return;
83
+ popupCloseHandledRef.current = true;
84
+ // Do not redock — keep undocked so the main Drawer stays closed.
85
+ onClose();
86
+ }, [clearPopupPoll, onClose]);
87
+ const closePopup = useCallback(() => {
88
+ clearPopupPoll();
89
+ const popup = popupRef.current;
90
+ if (popup && !popup.closed) {
91
+ ignorePopupCloseRef.current = true;
92
+ popup.close();
93
+ }
94
+ popupRef.current = null;
95
+ setExternalContainer(null);
96
+ }, [clearPopupPoll]);
97
+ const handleClose = useCallback(() => {
98
+ closePopup();
99
+ onClose();
100
+ }, [closePopup, onClose]);
101
+ const openUndockedWindow = useCallback(() => {
102
+ const existing = popupRef.current;
103
+ if (existing && !existing.closed) {
104
+ existing.focus();
105
+ setDockPosition('undocked');
106
+ CommentUtils.persistDockPosition('undocked');
107
+ return;
108
+ }
109
+ const opened = CommentUtils.openCommentsPopup();
110
+ if (!opened)
111
+ return;
112
+ const { popup, container } = opened;
113
+ ignorePopupCloseRef.current = false;
114
+ popupCloseHandledRef.current = false;
115
+ popupRef.current = popup;
116
+ setExternalContainer(container);
117
+ setDockPosition('undocked');
118
+ CommentUtils.persistDockPosition('undocked');
119
+ popup.addEventListener('beforeunload', () => {
120
+ handlePopupClosed();
121
+ });
122
+ clearPopupPoll();
123
+ popupPollRef.current = window.setInterval(() => {
124
+ if (!popupRef.current || popupRef.current.closed) {
125
+ handlePopupClosed();
126
+ }
127
+ }, 500);
128
+ }, [clearPopupPoll, handlePopupClosed]);
129
+ const setDockedPosition = useCallback((position) => {
130
+ lastDockedRef.current = position;
131
+ CommentUtils.persistDockPosition(position);
132
+ CommentUtils.persistLastDocked(position);
133
+ closePopup();
134
+ setDockPosition(position);
135
+ }, [closePopup]);
136
+ const dockMenuOptions = useMemo(() => [
137
+ {
138
+ label: t('menu.undock'),
139
+ icon: 'open_in_new',
140
+ disabled: dockPosition === 'undocked',
141
+ onClick: () => {
142
+ if (dockPosition === 'undocked')
143
+ return;
144
+ openUndockedWindow();
145
+ },
146
+ },
147
+ {
148
+ label: t('menu.dock.left'),
149
+ icon: 'dock_to_left',
150
+ disabled: dockPosition === 'left',
151
+ onClick: () => {
152
+ if (dockPosition === 'left')
153
+ return;
154
+ setDockedPosition('left');
155
+ },
156
+ },
157
+ {
158
+ label: t('menu.dock.bottom'),
159
+ icon: 'dock_to_bottom',
160
+ disabled: dockPosition === 'bottom',
161
+ onClick: () => {
162
+ if (dockPosition === 'bottom')
163
+ return;
164
+ setDockedPosition('bottom');
165
+ },
166
+ },
167
+ {
168
+ label: t('menu.dock.right'),
169
+ icon: 'dock_to_right',
170
+ disabled: dockPosition === 'right',
171
+ onClick: () => {
172
+ if (dockPosition === 'right')
173
+ return;
174
+ setDockedPosition('right');
175
+ },
176
+ },
177
+ ], [dockPosition, openUndockedWindow, setDockedPosition, t]);
72
178
  useEffect(() => {
73
179
  load();
74
180
  }, []);
181
+ useEffect(() => {
182
+ if (restoredUndockRef.current)
183
+ return;
184
+ if (CommentUtils.readStoredDockPosition() !== 'undocked')
185
+ return;
186
+ restoredUndockRef.current = true;
187
+ openUndockedWindow();
188
+ }, [openUndockedWindow]);
75
189
  useEffect(() => {
76
190
  if (!openAddComment || !comments)
77
191
  return;
@@ -83,26 +197,55 @@ export default function Comments({ uid, keywords, references, mode, onClose, onC
83
197
  });
84
198
  return () => cancelAnimationFrame(frame);
85
199
  }, [openAddComment, comments, comment, reply]);
200
+ useEffect(() => {
201
+ const onHostUnload = () => {
202
+ closePopup();
203
+ };
204
+ window.addEventListener('pagehide', onHostUnload);
205
+ window.addEventListener('beforeunload', onHostUnload);
206
+ return () => {
207
+ window.removeEventListener('pagehide', onHostUnload);
208
+ window.removeEventListener('beforeunload', onHostUnload);
209
+ closePopup();
210
+ };
211
+ }, [closePopup]);
86
212
  const handleResizePointerDown = useCallback((event) => {
87
213
  event.preventDefault();
88
214
  const handle = event.currentTarget;
89
215
  handle.setPointerCapture(event.pointerId);
90
216
  const previousUserSelect = document.body.style.userSelect;
91
217
  const previousCursor = document.body.style.cursor;
218
+ const isBottom = dockPosition === 'bottom';
219
+ const isLeft = dockPosition === 'left';
92
220
  document.body.style.userSelect = 'none';
93
- document.body.style.cursor = 'col-resize';
221
+ document.body.style.cursor = isBottom ? 'row-resize' : 'col-resize';
222
+ const computeSize = (clientX, clientY) => {
223
+ if (isBottom) {
224
+ return CommentUtils.clampDrawerHeight(window.innerHeight - clientY);
225
+ }
226
+ if (isLeft) {
227
+ return CommentUtils.clampDrawerWidth(clientX);
228
+ }
229
+ return CommentUtils.clampDrawerWidth(window.innerWidth - clientX);
230
+ };
94
231
  const onPointerMove = (moveEvent) => {
95
- const nextWidth = clampDrawerWidth(window.innerWidth - moveEvent.clientX);
96
- setDrawerWidth(nextWidth);
232
+ const next = computeSize(moveEvent.clientX, moveEvent.clientY);
233
+ if (isBottom) {
234
+ setDrawerHeight(next);
235
+ }
236
+ else {
237
+ setDrawerWidth(next);
238
+ }
97
239
  };
98
240
  const onPointerUp = (upEvent) => {
99
- const nextWidth = clampDrawerWidth(window.innerWidth - upEvent.clientX);
100
- setDrawerWidth(nextWidth);
101
- try {
102
- localStorage.setItem(DRAWER_WIDTH_STORAGE_KEY, String(nextWidth));
241
+ const next = computeSize(upEvent.clientX, upEvent.clientY);
242
+ if (isBottom) {
243
+ setDrawerHeight(next);
244
+ CommentUtils.persistDrawerHeight(next);
103
245
  }
104
- catch {
105
- // ignore storage access errors
246
+ else {
247
+ setDrawerWidth(next);
248
+ CommentUtils.persistDrawerWidth(next);
106
249
  }
107
250
  document.body.style.userSelect = previousUserSelect;
108
251
  document.body.style.cursor = previousCursor;
@@ -114,41 +257,54 @@ export default function Comments({ uid, keywords, references, mode, onClose, onC
114
257
  handle.addEventListener('pointermove', onPointerMove);
115
258
  handle.addEventListener('pointerup', onPointerUp);
116
259
  handle.addEventListener('pointercancel', onPointerUp);
117
- }, []);
118
- return (_jsx(Drawer, { anchor: "right", open: true, onClose: onClose, PaperProps: {
119
- className: 'comments-drawer-paper',
120
- style: { width: drawerWidth },
121
- }, children: _jsxs("div", { className: "comments-container", id: "comment-top", children: [_jsx("div", { className: "comments-resize-handle", onPointerDown: handleResizePointerDown, role: "separator", "aria-orientation": "vertical", "aria-label": "Resize comments panel" }), !comments ? (_jsx(AppLoading, { pageLoading: true })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "header", children: [_jsx(IconButton, { name: "close", size: "small", mode: "ghost", onClick: onClose }), _jsx("span", { className: "title", children: t('label.title.comments') }), _jsx("div", { className: "actions", children: hasEditAccess && (_jsx(Button, { text: t('button.add'), icon: "add", mode: "outlined", size: "small", className: "edit-button", onClick: () => {
122
- setComment(undefined);
123
- setOpenAddComment(!openAddComment);
124
- } })) })] }), _jsxs("div", { className: "body", children: [comments?.length === 0 && (_jsx(AppPlaceholder, { text: t('placeholder.comments.empty'), useMarginTop: true })), _jsx(CommentThread, { comments: comments, hasEditAccess: hasEditAccess, onRemove: (id) => {
125
- const updated = comments.filter((c) => c.id !== id);
126
- setComments(updated);
127
- onChange?.(updated);
128
- }, onUpdate: (thisComment) => {
129
- const updated = comments.map((c) => c.id === thisComment.id ? { ...c, ...thisComment } : c);
130
- setComments(updated);
131
- onChange?.(updated);
132
- }, onEdit: (el) => {
133
- setComment(el);
134
- setOpenAddComment(true);
135
- }, onReply: (el) => {
136
- setReply(el);
137
- setOpenAddComment(true);
138
- } }), hasEditAccess && openAddComment && (_jsx("div", { ref: commentAddRef, children: _jsx(CommentAdd, { uid: uid, mode: mode, users: users, references: references, keywords: keywords, comment: comment, reply: reply, onClose: () => {
139
- setComment(undefined);
140
- setReply(undefined);
141
- setOpenAddComment(false);
142
- }, onChange: (el) => {
143
- if (!comment) {
144
- const updated = [...(comments || []), el];
145
- setComments(updated);
146
- onChange?.(updated);
147
- }
148
- else {
149
- const updated = comments?.map((c) => c.id === comment.id ? el : c) || [];
150
- setComments(updated);
151
- onChange?.(updated);
152
- }
153
- } }) }))] })] }))] }) }));
260
+ }, [dockPosition]);
261
+ const paperStyle = dockPosition === 'bottom'
262
+ ? { height: drawerHeight, width: '100%' }
263
+ : { width: drawerWidth };
264
+ const content = !comments ? (_jsx(AppLoading, { pageLoading: true })) : (_jsxs(_Fragment, { children: [_jsx(CommentsHeader, { hasEditAccess: hasEditAccess, onToggleAdd: () => {
265
+ setComment(undefined);
266
+ setOpenAddComment(!openAddComment);
267
+ }, onClose: handleClose, dockMenuOptions: dockMenuOptions }), _jsxs("div", { className: "body", children: [comments.length === 0 && !openAddComment && (_jsx(AppPlaceholder, { text: t('placeholder.comments.empty'), useMarginTop: true })), _jsx(CommentThread, { comments: comments, hasEditAccess: hasEditAccess, onRemove: (id) => {
268
+ const updated = comments.filter((c) => c.id !== id);
269
+ setComments(updated);
270
+ onChange?.(updated);
271
+ }, onUpdate: (thisComment) => {
272
+ const updated = comments.map((c) => c.id === thisComment.id ? { ...c, ...thisComment } : c);
273
+ setComments(updated);
274
+ onChange?.(updated);
275
+ }, onEdit: (el) => {
276
+ setComment(el);
277
+ setOpenAddComment(true);
278
+ }, onReply: (el) => {
279
+ setReply(el);
280
+ setOpenAddComment(true);
281
+ } }), hasEditAccess && openAddComment && (_jsx("div", { ref: commentAddRef, children: _jsx(CommentAdd, { uid: uid, mode: mode, users: users, references: references, keywords: keywords, comment: comment, reply: reply, onClose: () => {
282
+ setComment(undefined);
283
+ setReply(undefined);
284
+ setOpenAddComment(false);
285
+ }, onChange: (el) => {
286
+ if (!comment) {
287
+ const updated = [...(comments || []), el];
288
+ setComments(updated);
289
+ onChange?.(updated);
290
+ }
291
+ else {
292
+ const updated = comments?.map((c) => (c.id === comment.id ? el : c)) || [];
293
+ setComments(updated);
294
+ onChange?.(updated);
295
+ }
296
+ } }) }))] })] }));
297
+ const panel = (_jsx(CommentsPanel, { dockPosition: dockPosition, showResizeHandle: CommentUtils.isDockedPosition(dockPosition), onResizePointerDown: handleResizePointerDown, children: content }));
298
+ const isDocked = CommentUtils.isDockedPosition(dockPosition);
299
+ const drawerAnchor = isDocked
300
+ ? dockPosition
301
+ : (lastDockedRef.current ?? 'right');
302
+ return (_jsxs(_Fragment, { children: [_jsx(Drawer, { anchor: drawerAnchor, open: isDocked, onClose: handleClose, slotProps: {
303
+ backdrop: { invisible: true },
304
+ }, PaperProps: {
305
+ className: cx('comments-drawer-paper', `dock-${drawerAnchor}`),
306
+ style: paperStyle,
307
+ }, children: isDocked ? panel : null }), dockPosition === 'undocked' &&
308
+ externalContainer &&
309
+ createPortal(_jsx("div", { className: "comments-undocked-root", children: panel }), externalContainer)] }));
154
310
  }
@@ -1,8 +1,26 @@
1
1
  .comments-drawer-paper {
2
- width: 40vw;
3
- max-width: 90vw;
4
- min-width: 320px;
5
2
  overflow: hidden;
3
+
4
+ &:not(.dock-bottom) {
5
+ width: 40vw;
6
+ max-width: 90vw;
7
+ min-width: 320px;
8
+ }
9
+
10
+ &.dock-bottom {
11
+ width: 100%;
12
+ max-width: none;
13
+ min-width: 0;
14
+ max-height: 90vh;
15
+ min-height: 200px;
16
+ }
17
+ }
18
+
19
+ .comments-undocked-root {
20
+ width: 100%;
21
+ height: 100%;
22
+ overflow: hidden;
23
+ background-color: var(--background-default);
6
24
  }
7
25
 
8
26
  .comments-container {
@@ -15,21 +33,12 @@
15
33
 
16
34
  > .comments-resize-handle {
17
35
  position: absolute;
18
- top: 0;
19
- left: 0;
20
- width: 6px;
21
- height: 100%;
22
- cursor: col-resize;
23
36
  z-index: 2;
24
37
  touch-action: none;
25
38
 
26
39
  &::after {
27
40
  content: '';
28
41
  position: absolute;
29
- top: 0;
30
- left: 0;
31
- width: 2px;
32
- height: 100%;
33
42
  background-color: transparent;
34
43
  transition: background-color 0.15s ease;
35
44
  }
@@ -40,6 +49,51 @@
40
49
  }
41
50
  }
42
51
 
52
+ &.dock-right > .comments-resize-handle {
53
+ top: 0;
54
+ left: 0;
55
+ width: 6px;
56
+ height: 100%;
57
+ cursor: col-resize;
58
+
59
+ &::after {
60
+ top: 0;
61
+ left: 0;
62
+ width: 2px;
63
+ height: 100%;
64
+ }
65
+ }
66
+
67
+ &.dock-left > .comments-resize-handle {
68
+ top: 0;
69
+ right: 0;
70
+ width: 6px;
71
+ height: 100%;
72
+ cursor: col-resize;
73
+
74
+ &::after {
75
+ top: 0;
76
+ right: 0;
77
+ width: 2px;
78
+ height: 100%;
79
+ }
80
+ }
81
+
82
+ &.dock-bottom > .comments-resize-handle {
83
+ top: 0;
84
+ left: 0;
85
+ width: 100%;
86
+ height: 6px;
87
+ cursor: row-resize;
88
+
89
+ &::after {
90
+ top: 0;
91
+ left: 0;
92
+ width: 100%;
93
+ height: 2px;
94
+ }
95
+ }
96
+
43
97
  > .header {
44
98
  display: flex;
45
99
  align-items: center;
@@ -54,10 +108,12 @@
54
108
 
55
109
  > .actions {
56
110
  display: flex;
111
+ align-items: center;
57
112
  margin-left: auto;
113
+ gap: var(--spacing-100);
58
114
 
59
115
  .edit-button {
60
- margin-right: var(--spacing-100);
116
+ margin-right: 0;
61
117
  }
62
118
  }
63
119
  }
@@ -181,19 +237,19 @@
181
237
  }
182
238
 
183
239
  @media (max-width: 1366px) {
184
- .comments-drawer-paper {
240
+ .comments-drawer-paper:not(.dock-bottom) {
185
241
  width: 40vw;
186
242
  }
187
243
  }
188
244
 
189
245
  @media (max-width: 768px) {
190
- .comments-drawer-paper {
246
+ .comments-drawer-paper:not(.dock-bottom) {
191
247
  width: 60vw;
192
248
  }
193
249
  }
194
250
 
195
251
  @media (max-width: 767px) {
196
- .comments-drawer-paper {
252
+ .comments-drawer-paper:not(.dock-bottom) {
197
253
  width: 90vw;
198
254
  }
199
255
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tycho-components",
3
3
  "private": false,
4
- "version": "0.41.0",
4
+ "version": "0.41.2",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {