@noya-app/noya-designsystem 0.1.9 → 0.1.11

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.
@@ -0,0 +1,315 @@
1
+ import {
2
+ Divider,
3
+ IconButton,
4
+ Small,
5
+ useCurrentFloatingWindowInternal,
6
+ useDesignSystemTheme,
7
+ useFloatingWindowManager,
8
+ } from "@noya-app/noya-designsystem";
9
+ import React, { useCallback, useRef, useState } from "react";
10
+
11
+ interface FloatingWindowProps {
12
+ title?: string;
13
+ onClose?: () => void;
14
+ children: React.ReactNode;
15
+ initialWidth?: number;
16
+ initialHeight?: number;
17
+ initialX?: number;
18
+ initialY?: number;
19
+ minWidth?: number;
20
+ minHeight?: number;
21
+ toolbarContent?: React.ReactNode;
22
+ renderToolbar?: (props: {
23
+ title?: string;
24
+ toolbarContent?: React.ReactNode;
25
+ onClose?: () => void;
26
+ }) => React.ReactNode;
27
+ }
28
+
29
+ const styles = {
30
+ noSelect: {
31
+ userSelect: "none",
32
+ WebkitUserSelect: "none",
33
+ MozUserSelect: "none",
34
+ msUserSelect: "none",
35
+ },
36
+ windowWrapper: {
37
+ position: "absolute",
38
+ backgroundColor: "white",
39
+ borderRadius: "4px",
40
+ boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
41
+ overflow: "hidden",
42
+ // backdropFilter: "blur(10px)",
43
+ },
44
+ toolbar: {
45
+ display: "flex",
46
+ alignItems: "center",
47
+ padding: "8px 12px",
48
+ cursor: "move",
49
+ userSelect: "none",
50
+ minHeight: "43px",
51
+ },
52
+ closeButton: {
53
+ background: "none",
54
+ border: "none",
55
+ fontSize: "16px",
56
+ cursor: "pointer",
57
+ },
58
+ content: {
59
+ padding: "12px",
60
+ height: "calc(100% - 44px)", // Adjust based on toolbar height
61
+ overflow: "auto",
62
+ display: "flex",
63
+ flexDirection: "column",
64
+ },
65
+ resizeHandle: {
66
+ position: "absolute",
67
+ },
68
+ } as const;
69
+
70
+ const resizeHandlePositions = {
71
+ top: { top: "0", left: "0", right: "0", height: "5px", cursor: "ns-resize" },
72
+ right: {
73
+ top: "0",
74
+ right: "0",
75
+ bottom: "0",
76
+ width: "5px",
77
+ cursor: "ew-resize",
78
+ },
79
+ bottom: {
80
+ bottom: "0",
81
+ left: "0",
82
+ right: "0",
83
+ height: "5px",
84
+ cursor: "ns-resize",
85
+ },
86
+ left: { top: "0", left: "0", bottom: "0", width: "5px", cursor: "ew-resize" },
87
+ topLeft: {
88
+ top: "0",
89
+ left: "0",
90
+ width: "10px",
91
+ height: "10px",
92
+ cursor: "nwse-resize",
93
+ },
94
+ topRight: {
95
+ top: "0",
96
+ right: "0",
97
+ width: "10px",
98
+ height: "10px",
99
+ cursor: "nesw-resize",
100
+ },
101
+ bottomLeft: {
102
+ bottom: "0",
103
+ left: "0",
104
+ width: "10px",
105
+ height: "10px",
106
+ cursor: "nesw-resize",
107
+ },
108
+ bottomRight: {
109
+ bottom: "0",
110
+ right: "0",
111
+ width: "10px",
112
+ height: "10px",
113
+ cursor: "nwse-resize",
114
+ },
115
+ };
116
+
117
+ function defaultRenderToolbar({
118
+ title,
119
+ toolbarContent,
120
+ onClose,
121
+ }: {
122
+ title?: string;
123
+ toolbarContent?: React.ReactNode;
124
+ onClose?: () => void;
125
+ }) {
126
+ return (
127
+ <>
128
+ <Small flex="1" fontWeight="500" color="text">
129
+ {title}
130
+ </Small>
131
+ {toolbarContent}
132
+ <IconButton
133
+ iconName="Cross3Icon"
134
+ onClick={onClose}
135
+ style={{
136
+ marginLeft: "6px",
137
+ marginRight: "-6px",
138
+ }}
139
+ />
140
+ </>
141
+ );
142
+ }
143
+
144
+ export const FloatingWindow: React.FC<FloatingWindowProps> = ({
145
+ title,
146
+ onClose,
147
+ children,
148
+ initialWidth = 300,
149
+ initialHeight = 200,
150
+ initialX = 100,
151
+ initialY = 100,
152
+ minWidth = 200,
153
+ minHeight = 100,
154
+ toolbarContent,
155
+ renderToolbar = defaultRenderToolbar,
156
+ }) => {
157
+ const [position, setPosition] = useState({ x: initialX, y: initialY });
158
+ const [size, setSize] = useState({
159
+ width: initialWidth,
160
+ height: initialHeight,
161
+ });
162
+ const [isDragging, setIsDragging] = useState(false);
163
+ const [isResizing, setIsResizing] = useState(false);
164
+ const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
165
+ const [resizeDirection, setResizeDirection] = useState<string | null>(null);
166
+ const wrapperRef = useRef<HTMLDivElement>(null);
167
+
168
+ const toggleGlobalTextSelection = (disable: boolean) => {
169
+ if (disable) {
170
+ document.body.style.userSelect = "none";
171
+ } else {
172
+ document.body.style.userSelect = "";
173
+ }
174
+ };
175
+
176
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
177
+ if (!wrapperRef.current) return;
178
+
179
+ const rect = wrapperRef.current.getBoundingClientRect();
180
+ setDragOffset({
181
+ x: e.clientX - rect.left,
182
+ y: e.clientY - rect.top,
183
+ });
184
+ setIsDragging(true);
185
+ }, []);
186
+
187
+ const handleResizeStart = useCallback(
188
+ (direction: string) => (e: React.MouseEvent) => {
189
+ e.stopPropagation();
190
+ setIsResizing(true);
191
+ setResizeDirection(direction);
192
+ toggleGlobalTextSelection(true);
193
+ },
194
+ []
195
+ );
196
+
197
+ const handleMouseMove = useCallback(
198
+ (e: MouseEvent) => {
199
+ if (isDragging) {
200
+ setPosition({
201
+ x: e.clientX - dragOffset.x,
202
+ y: e.clientY - dragOffset.y,
203
+ });
204
+ } else if (isResizing && resizeDirection) {
205
+ const rect = wrapperRef.current!.getBoundingClientRect();
206
+
207
+ let newWidth = size.width;
208
+ let newHeight = size.height;
209
+ let newX = position.x;
210
+ let newY = position.y;
211
+
212
+ if (/right/i.test(resizeDirection)) {
213
+ newWidth = Math.max(e.clientX - rect.left, minWidth);
214
+ }
215
+ if (/bottom/i.test(resizeDirection)) {
216
+ newHeight = Math.max(e.clientY - rect.top, minHeight);
217
+ }
218
+ if (/left/i.test(resizeDirection)) {
219
+ const potentialWidth = rect.right - e.clientX;
220
+ if (potentialWidth <= minWidth) {
221
+ newWidth = minWidth;
222
+ newX = rect.right - minWidth;
223
+ } else {
224
+ newWidth = potentialWidth;
225
+ newX = e.clientX;
226
+ }
227
+ }
228
+ if (/top/i.test(resizeDirection)) {
229
+ const potentialHeight = rect.bottom - e.clientY;
230
+ if (potentialHeight <= minHeight) {
231
+ newHeight = minHeight;
232
+ newY = rect.bottom - minHeight;
233
+ } else {
234
+ newHeight = potentialHeight;
235
+ newY = e.clientY;
236
+ }
237
+ }
238
+
239
+ setSize({ width: newWidth, height: newHeight });
240
+ setPosition({ x: newX, y: newY });
241
+ }
242
+ },
243
+ [
244
+ isDragging,
245
+ isResizing,
246
+ dragOffset,
247
+ resizeDirection,
248
+ size,
249
+ position,
250
+ minWidth,
251
+ minHeight,
252
+ ]
253
+ );
254
+
255
+ const handleMouseUp = useCallback((e: MouseEvent) => {
256
+ setIsDragging(false);
257
+ setIsResizing(false);
258
+ setResizeDirection(null);
259
+ toggleGlobalTextSelection(false);
260
+ }, []);
261
+
262
+ React.useEffect(() => {
263
+ document.addEventListener("mousemove", handleMouseMove);
264
+ document.addEventListener("mouseup", handleMouseUp);
265
+
266
+ return () => {
267
+ document.removeEventListener("mousemove", handleMouseMove);
268
+ document.removeEventListener("mouseup", handleMouseUp);
269
+ };
270
+ }, [handleMouseMove, handleMouseUp]);
271
+
272
+ const theme = useDesignSystemTheme();
273
+
274
+ const floatingWindowManager = useFloatingWindowManager();
275
+ const currentFloatingWindow = useCurrentFloatingWindowInternal();
276
+
277
+ const handleClose = useCallback(() => {
278
+ if (onClose) onClose();
279
+ floatingWindowManager.closeWindow(currentFloatingWindow.id);
280
+ }, [currentFloatingWindow.id, floatingWindowManager, onClose]);
281
+
282
+ return (
283
+ <div
284
+ ref={wrapperRef}
285
+ style={{
286
+ ...styles.windowWrapper,
287
+ top: position.y,
288
+ left: position.x,
289
+ width: size.width,
290
+ height: size.height,
291
+ outline: `1px solid ${theme.colors.divider}`,
292
+ backgroundColor: theme.colors.canvas.background,
293
+ }}
294
+ >
295
+ <div
296
+ style={{
297
+ ...styles.toolbar,
298
+ backgroundColor: theme.colors.sidebar.background,
299
+ }}
300
+ onMouseDown={handleMouseDown}
301
+ >
302
+ {renderToolbar({ title, toolbarContent, onClose: handleClose })}
303
+ </div>
304
+ <Divider />
305
+ <div style={styles.content}>{children}</div>
306
+ {Object.entries(resizeHandlePositions).map(([direction, style]) => (
307
+ <div
308
+ key={direction}
309
+ style={{ ...styles.resizeHandle, ...style }}
310
+ onMouseDown={handleResizeStart(direction)}
311
+ />
312
+ ))}
313
+ </div>
314
+ );
315
+ };
@@ -13,6 +13,7 @@ const elements: Record<keyof Theme["textStyles"], keyof ReactHTML> = {
13
13
  heading5: "h5",
14
14
  body: "p",
15
15
  small: "span",
16
+ button: "span",
16
17
  code: "code",
17
18
  label: "span",
18
19
  };
@@ -68,7 +69,7 @@ const StyledElement = styled.span<
68
69
  styleProps: StyleProps;
69
70
  } & Pick<Props, "variant" | "breakpoints" | "color">
70
71
  >(({ theme, variant, styleProps, breakpoints, color }) => ({
71
- ...theme.textStyles[variant],
72
+ ...(theme.textStyles as any)[variant],
72
73
  color: color ? theme.colors[color] : undefined,
73
74
  ...styleProps,
74
75
  ...mergeBreakpoints(breakpoints || []),
@@ -83,7 +83,7 @@ export const styles = {
83
83
  separatorStyle: ({ theme }: { theme: Theme }): CSSObject => ({
84
84
  height: "1px",
85
85
  backgroundColor: theme.colors.divider,
86
- margin: "4px 8px",
86
+ margin: "4px -4px",
87
87
  }),
88
88
 
89
89
  itemStyle: ({
@@ -93,14 +93,12 @@ export const styles = {
93
93
  theme: Theme;
94
94
  disabled?: boolean;
95
95
  }): CSSObject => ({
96
- ...theme.textStyles.small,
97
- fontWeight: 500,
98
- fontSize: "0.8rem",
96
+ ...theme.textStyles.button,
99
97
  flex: "0 0 auto",
100
98
  userSelect: "none",
101
99
  cursor: "pointer",
102
100
  borderRadius: "3px",
103
- padding: "4px 8px",
101
+ padding: "6px 8px",
104
102
  ...(disabled && {
105
103
  color: theme.colors.textDisabled,
106
104
  }),
@@ -1,15 +1,16 @@
1
- import { PlatformName } from '@noya-app/noya-keymap';
1
+ import { PlatformName } from "@noya-app/noya-keymap";
2
2
  import React, {
3
3
  createContext,
4
4
  memo,
5
5
  ReactNode,
6
6
  useContext,
7
7
  useMemo,
8
- } from 'react';
9
- import { ThemeProvider, useTheme } from 'styled-components';
10
- import { ToastProvider } from '../components/Toast';
11
- import { Theme } from '../theme';
12
- import { DialogProvider } from './DialogContext';
8
+ } from "react";
9
+ import { ThemeProvider, useTheme } from "styled-components";
10
+ import { ToastProvider } from "../components/Toast";
11
+ import { Theme } from "../theme";
12
+ import { DialogProvider } from "./DialogContext";
13
+ import { FloatingWindowProvider } from "./FloatingWindowContext";
13
14
 
14
15
  export type DesignSystemConfigurationContextValue = {
15
16
  platform: PlatformName;
@@ -33,20 +34,22 @@ export const DesignSystemConfigurationProvider = memo(
33
34
  return (
34
35
  <DesignSystemConfigurationContext.Provider value={contextValue}>
35
36
  <ThemeProvider theme={theme}>
36
- <DialogProvider>
37
- <ToastProvider>{children}</ToastProvider>
38
- </DialogProvider>
37
+ <FloatingWindowProvider>
38
+ <DialogProvider>
39
+ <ToastProvider>{children}</ToastProvider>
40
+ </DialogProvider>
41
+ </FloatingWindowProvider>
39
42
  </ThemeProvider>
40
43
  </DesignSystemConfigurationContext.Provider>
41
44
  );
42
- },
45
+ }
43
46
  );
44
47
 
45
48
  export function useDesignSystemConfiguration(): DesignSystemConfigurationContextValue {
46
49
  const value = useContext(DesignSystemConfigurationContext);
47
50
 
48
51
  if (!value) {
49
- throw new Error('Missing DesignSystemConfigurationProvider');
52
+ throw new Error("Missing DesignSystemConfigurationProvider");
50
53
  }
51
54
 
52
55
  return value;
@@ -0,0 +1,85 @@
1
+ import { uuid } from "@noya-app/noya-utils";
2
+ import React, { createContext, useContext, useMemo, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+
5
+ type FloatingWindowContextValue = {
6
+ createWindow: (element: React.ReactNode) => string;
7
+ closeWindow: (id: string) => void;
8
+ };
9
+
10
+ const FloatingWindowContext = createContext<
11
+ FloatingWindowContextValue | undefined
12
+ >(undefined);
13
+
14
+ type CurrentWindowContextValue = {
15
+ id: string;
16
+ };
17
+
18
+ const CurrentWindowContext = createContext<
19
+ CurrentWindowContextValue | undefined
20
+ >(undefined);
21
+
22
+ export const FloatingWindowProvider = ({
23
+ children,
24
+ }: {
25
+ children: React.ReactNode;
26
+ }) => {
27
+ const [windows, setWindows] = useState<Record<string, React.ReactNode>>({});
28
+
29
+ const contextValue = useMemo(() => {
30
+ const createWindow = (element: React.ReactNode) => {
31
+ const id = uuid();
32
+
33
+ setWindows((windows) => ({
34
+ ...windows,
35
+ [id]: (
36
+ <CurrentWindowContext.Provider value={{ id }}>
37
+ {element}
38
+ </CurrentWindowContext.Provider>
39
+ ),
40
+ }));
41
+
42
+ return id;
43
+ };
44
+
45
+ const closeWindow = (id: string) => {
46
+ setWindows((windows) => {
47
+ const { [id]: _, ...rest } = windows;
48
+ return rest;
49
+ });
50
+ };
51
+
52
+ return { createWindow, closeWindow };
53
+ }, []);
54
+
55
+ return (
56
+ <FloatingWindowContext.Provider value={contextValue}>
57
+ {children}
58
+ {Object.entries(windows).map(([id, element]) =>
59
+ createPortal(element, document.body, id)
60
+ )}
61
+ </FloatingWindowContext.Provider>
62
+ );
63
+ };
64
+
65
+ export const useFloatingWindowManager = () => {
66
+ const context = useContext(FloatingWindowContext);
67
+
68
+ if (!context) {
69
+ throw new Error(
70
+ "useFloatingWindow must be used within a FloatingWindowProvider"
71
+ );
72
+ }
73
+
74
+ return context;
75
+ };
76
+
77
+ export const useCurrentFloatingWindowInternal = () => {
78
+ const context = useContext(CurrentWindowContext);
79
+
80
+ if (!context) {
81
+ throw new Error("useCurrentWindow must be used within a FloatingWindow");
82
+ }
83
+
84
+ return context;
85
+ };
package/src/index.tsx CHANGED
@@ -25,6 +25,7 @@ export * from "./components/DraggableMenuButton";
25
25
  export * from "./components/DropdownMenu";
26
26
  export * from "./components/FillInputField";
27
27
  export * from "./components/FillPreviewBackground";
28
+ export * from "./components/FloatingWindow";
28
29
  export * from "./components/GradientPicker";
29
30
  export * from "./components/Grid";
30
31
  export * from "./components/GridView";
@@ -55,10 +56,11 @@ export type {
55
56
  MenuItem,
56
57
  RegularMenuItem,
57
58
  } from "./components/internal/Menu";
59
+ // Contexts
58
60
  export * from "./contexts/DesignSystemConfiguration";
59
61
  export * from "./contexts/DialogContext";
62
+ export * from "./contexts/FloatingWindowContext";
60
63
  export * from "./contexts/ImageDataContext";
61
- // Contexts
62
64
  export * from "./contexts/GlobalInputBlurContext";
63
65
  // Hooks
64
66
  export * from "./hooks/useHover";
package/src/theme/dark.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import produce from "immer";
2
2
  import * as lightTheme from "./light";
3
3
 
4
+ export const name: string = "dark";
5
+
4
6
  export const colors = produce(lightTheme.colors, (colors) => {
5
7
  colors.logo.fill = "rgb(248,248,250)";
6
8
  colors.logo.highlight = "rgb(248,248,250)";
@@ -1,6 +1,8 @@
1
1
  import { CSSObject } from "styled-components";
2
2
  import { mediaQuery } from "../mediaQuery";
3
3
 
4
+ export const name: string = "light";
5
+
4
6
  export const colors = {
5
7
  logo: {
6
8
  fill: "rgb(175,175,175)",
@@ -161,6 +163,13 @@ export const textStyles = {
161
163
  fontWeight: 400,
162
164
  lineHeight: "1.4",
163
165
  } as CSSObject,
166
+ button: {
167
+ fontFamily: fonts.normal,
168
+ fontSize: "0.8rem",
169
+ fontWeight: 500,
170
+ lineHeight: "1.4",
171
+ letterSpacing: "-0.2px",
172
+ } as CSSObject,
164
173
  code: {
165
174
  fontFamily: fonts.monospace,
166
175
  fontSize: "90%",