@noya-app/noya-designsystem 0.1.63 → 0.1.65
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/.turbo/turbo-build.log +13 -10
- package/CHANGELOG.md +25 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +648 -333
- package/dist/index.d.ts +648 -333
- package/dist/index.js +13813 -9157
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13949 -9269
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -22
- package/src/__tests__/validateDropIndicator.test.ts +26 -41
- package/src/__tests__/workspaceLayout.test.ts +281 -0
- package/src/components/ActionMenu.tsx +2 -0
- package/src/components/ActivityIndicator.tsx +13 -6
- package/src/components/ActivityLog.tsx +197 -0
- package/src/components/AnimatePresence.tsx +43 -26
- package/src/components/Avatar.tsx +45 -26
- package/src/components/Banner.tsx +57 -11
- package/src/components/BaseToolbar.tsx +67 -34
- package/src/components/Breadcrumbs.tsx +4 -2
- package/src/components/Button.tsx +28 -23
- package/src/components/Checkbox.tsx +6 -35
- package/src/components/Chip.tsx +47 -33
- package/src/components/Collection.tsx +20 -12
- package/src/components/ColorSwatch.tsx +42 -22
- package/src/components/ColorSwatchControl.tsx +7 -5
- package/src/components/Combobox.tsx +8 -6
- package/src/components/ComboboxMenu.tsx +6 -5
- package/src/components/CommandPalette.tsx +3 -1
- package/src/components/ContextMenu.tsx +3 -1
- package/src/components/Dialog.tsx +9 -13
- package/src/components/DimensionInput.tsx +2 -0
- package/src/components/Divider.tsx +18 -14
- package/src/components/DraggableMenuButton.tsx +11 -5
- package/src/components/Drawer.tsx +14 -6
- package/src/components/DropdownMenu.tsx +4 -2
- package/src/components/EditableText.tsx +4 -2
- package/src/components/Fade.tsx +13 -13
- package/src/components/FileUploadIndicator.tsx +108 -0
- package/src/components/FloatingWindow.tsx +4 -2
- package/src/components/Grid.tsx +58 -42
- package/src/components/GridView.tsx +65 -36
- package/src/components/IVirtualizedList.tsx +5 -0
- package/src/components/IconButton.tsx +2 -0
- package/src/components/Icons.tsx +24 -4
- package/src/components/InputField.tsx +21 -19
- package/src/components/InspectorContainer.tsx +2 -2
- package/src/components/InspectorPrimitives.tsx +15 -9
- package/src/components/Label.tsx +4 -3
- package/src/components/LabeledElementView.tsx +9 -5
- package/src/components/LabeledField.tsx +8 -6
- package/src/components/List.tsx +112 -46
- package/src/components/ListMenu.tsx +91 -0
- package/src/components/ListNavigator.tsx +106 -0
- package/src/components/ListView.tsx +22 -1147
- package/src/components/MediaThumbnail.tsx +84 -14
- package/src/components/Message.tsx +11 -12
- package/src/components/Navigator.tsx +461 -0
- package/src/components/NoyaLogo.tsx +1 -1
- package/src/components/Popover.tsx +12 -6
- package/src/components/Progress.tsx +31 -8
- package/src/components/ResizableContainer.tsx +187 -0
- package/src/components/RingProgress.tsx +128 -0
- package/src/components/ScrollArea.tsx +7 -5
- package/src/components/ScrollableSidebar.tsx +10 -0
- package/src/components/SearchCompletionMenu.tsx +5 -3
- package/src/components/Section.tsx +109 -57
- package/src/components/SegmentedControl.tsx +51 -25
- package/src/components/SelectMenu.tsx +16 -21
- package/src/components/SelectionToolbar.tsx +7 -4
- package/src/components/Slider.tsx +12 -10
- package/src/components/Spacer.tsx +2 -2
- package/src/components/StackNavigator.tsx +378 -0
- package/src/components/Stepper.tsx +88 -0
- package/src/components/Switch.tsx +7 -5
- package/src/components/Tabs.tsx +6 -4
- package/src/components/Text.tsx +29 -18
- package/src/components/TextArea.tsx +5 -3
- package/src/components/Toast.tsx +5 -5
- package/src/components/Toolbar.tsx +57 -10
- package/src/components/ToolbarDrawer.tsx +29 -0
- package/src/components/Tooltip.tsx +2 -2
- package/src/components/TreeView.tsx +13 -4
- package/src/components/UserPointer.tsx +6 -4
- package/src/components/ai-assistant/AIAssistantLayout.tsx +10 -8
- package/src/components/blocks/ImageBlockComponent.tsx +162 -0
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +6 -4
- package/src/components/file-explorer/FileExplorerLayout.tsx +22 -8
- package/src/components/internal/Checkmark.tsx +24 -0
- package/src/components/internal/Menu.tsx +24 -19
- package/src/components/internal/MenuViewport.tsx +15 -6
- package/src/components/internal/SelectItem.tsx +7 -26
- package/src/components/internal/TextInput.tsx +7 -1
- package/src/components/listView/ListViewContexts.tsx +68 -0
- package/src/components/listView/ListViewEditableRowTitle.tsx +55 -0
- package/src/components/listView/ListViewRoot.tsx +521 -0
- package/src/components/listView/ListViewRow.tsx +475 -0
- package/src/components/listView/ListViewRowTitle.tsx +21 -0
- package/src/components/listView/types.ts +11 -0
- package/src/components/pipeline/PipelineResultLayout.tsx +6 -6
- package/src/components/sorting/DragRegistration.tsx +10 -5
- package/src/components/sorting/SharedDragProvider.tsx +134 -27
- package/src/components/sorting/Sortable.tsx +90 -27
- package/src/components/sorting/createSharedDrag.tsx +7 -6
- package/src/components/sorting/sorting.ts +6 -9
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +110 -29
- package/src/components/workspace/PanelWorkspaceLayout.tsx +148 -99
- package/src/components/workspace/VerticalTabMenu.tsx +46 -0
- package/src/components/workspace/WorkspaceLayout.tsx +76 -63
- package/src/components/workspace/constants.ts +4 -0
- package/src/components/workspace/panelStorage.ts +216 -0
- package/src/components/workspace/types.ts +11 -4
- package/src/contexts/DesignSystemConfiguration.tsx +12 -5
- package/src/contexts/DialogContext.tsx +19 -5
- package/src/contexts/FloatingWindowContext.tsx +2 -0
- package/src/contexts/GlobalInputBlurContext.tsx +2 -0
- package/src/contexts/ImageDataContext.tsx +2 -0
- package/src/contexts/LinkComponentContext.tsx +17 -0
- package/src/contexts/OpenPortalsContext.tsx +2 -0
- package/src/contexts/PortalScopeContext.tsx +2 -0
- package/src/hooks/mergeEventHandlers.ts +1 -1
- package/src/hooks/useHover.ts +30 -17
- package/src/hooks/useIndent.ts +5 -8
- package/src/hooks/useLabel.ts +2 -0
- package/src/hooks/useObjectURL.ts +5 -3
- package/src/hooks/usePlatform.ts +5 -3
- package/src/hooks/useTheme.ts +3 -1
- package/src/index.css +43 -10
- package/src/index.tsx +15 -7
- package/src/utils/classNames.ts +56 -9
- package/src/utils/combobox.ts +2 -0
- package/src/utils/editableBlockStyles.ts +2 -2
- package/src/utils/formatByteSize.ts +9 -0
- package/src/utils/inputs.ts +8 -6
- package/src/utils/sketchColor.ts +0 -34
- package/src/utils/withSeparatorElements.ts +1 -1
- package/tailwind.config.ts +1 -0
- package/src/components/FillInputField.tsx +0 -37
- package/src/components/FillPreviewBackground.tsx +0 -132
- package/src/components/GradientPicker.tsx +0 -88
- package/src/hooks/usePreservePanelSize.tsx +0 -183
- package/src/mediaQuery.ts +0 -13
- package/src/utils/getGradientBackground.tsx +0 -31
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Size } from "@noya-app/noya-geometry";
|
|
1
4
|
import {
|
|
2
5
|
FileIcon,
|
|
3
6
|
ImageIcon,
|
|
7
|
+
NoyaIcon,
|
|
4
8
|
SpeakerLoudIcon,
|
|
5
9
|
VideoIcon,
|
|
6
10
|
} from "@noya-app/noya-icons";
|
|
@@ -23,7 +27,12 @@ const getAssetType = (contentType?: string): AssetType | undefined => {
|
|
|
23
27
|
return undefined;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
|
-
type
|
|
30
|
+
export type RenderThumbnailIconProps = {
|
|
31
|
+
fileName?: string;
|
|
32
|
+
contentType?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type MediaThumbnailProps = {
|
|
27
36
|
contentType?: string;
|
|
28
37
|
url?: string;
|
|
29
38
|
selected?: boolean;
|
|
@@ -39,6 +48,9 @@ type MediaThumbnailProps = {
|
|
|
39
48
|
size?: CollectionItemSize;
|
|
40
49
|
|
|
41
50
|
fileName?: string;
|
|
51
|
+
renderThumbnailIcon?: (props: RenderThumbnailIconProps) => React.ReactNode;
|
|
52
|
+
dimensions?: Size;
|
|
53
|
+
aspect?: "contain" | "cover";
|
|
42
54
|
};
|
|
43
55
|
|
|
44
56
|
export const getThumbnailColors = (
|
|
@@ -86,6 +98,9 @@ export const MediaThumbnail = memo(function MediaThumbnail({
|
|
|
86
98
|
iconName,
|
|
87
99
|
size = "medium",
|
|
88
100
|
fileName,
|
|
101
|
+
dimensions,
|
|
102
|
+
renderThumbnailIcon,
|
|
103
|
+
aspect = "contain",
|
|
89
104
|
}: MediaThumbnailProps) {
|
|
90
105
|
const { icon: iconColor, background: backgroundColor } = getThumbnailColors(
|
|
91
106
|
colorScheme,
|
|
@@ -101,18 +116,47 @@ export const MediaThumbnail = memo(function MediaThumbnail({
|
|
|
101
116
|
);
|
|
102
117
|
|
|
103
118
|
const content = useMemo(() => {
|
|
119
|
+
if (renderThumbnailIcon) {
|
|
120
|
+
const rendered = renderThumbnailIcon({ fileName, contentType });
|
|
121
|
+
|
|
122
|
+
if (rendered) return rendered;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (url) {
|
|
126
|
+
// const renderUrl = new URL(`http://localhost:8080/render`);
|
|
127
|
+
const renderUrl = new URL(`https://image.noya.io/render`);
|
|
128
|
+
|
|
129
|
+
const imageDimensions = dimensions ?? {
|
|
130
|
+
width: imageSizeMap[size],
|
|
131
|
+
height: imageSizeMap[size],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (imageDimensions.width) {
|
|
135
|
+
renderUrl.searchParams.set("w", imageDimensions.width?.toString());
|
|
136
|
+
}
|
|
137
|
+
if (imageDimensions.height) {
|
|
138
|
+
renderUrl.searchParams.set("h", imageDimensions.height?.toString());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
renderUrl.searchParams.set("url", url);
|
|
142
|
+
renderUrl.searchParams.set("fit", aspect);
|
|
143
|
+
renderUrl.searchParams.set("theme", "light");
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<img
|
|
147
|
+
src={renderUrl.toString()}
|
|
148
|
+
alt=""
|
|
149
|
+
className="n-object-contain n-w-full n-h-full"
|
|
150
|
+
tabIndex={-1}
|
|
151
|
+
width={imageDimensions?.width}
|
|
152
|
+
height={imageDimensions?.height}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
104
157
|
switch (contentType) {
|
|
105
158
|
case "image":
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<img
|
|
110
|
-
src={url}
|
|
111
|
-
alt=""
|
|
112
|
-
className="object-contain w-full h-full"
|
|
113
|
-
tabIndex={-1}
|
|
114
|
-
/>
|
|
115
|
-
);
|
|
159
|
+
return <ImageIcon color={iconColor} style={iconStyles} />;
|
|
116
160
|
case "video":
|
|
117
161
|
return <VideoIcon color={iconColor} style={iconStyles} />;
|
|
118
162
|
case "audio":
|
|
@@ -124,13 +168,18 @@ export const MediaThumbnail = memo(function MediaThumbnail({
|
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
if (fileName) {
|
|
171
|
+
if (fileName.endsWith(".noya")) {
|
|
172
|
+
return <NoyaIcon color={iconColor} style={iconStyles} />;
|
|
173
|
+
}
|
|
174
|
+
|
|
127
175
|
const fileIcon = findFileIcon(fileName);
|
|
176
|
+
|
|
128
177
|
if (fileIcon) {
|
|
129
178
|
return (
|
|
130
179
|
<img
|
|
131
180
|
src={`https://api.iconify.design/catppuccin/${fileIcon}.svg?height=none&box=1`}
|
|
132
181
|
alt=""
|
|
133
|
-
className="w-4 h-4"
|
|
182
|
+
className="n-w-4 n-h-4"
|
|
134
183
|
/>
|
|
135
184
|
);
|
|
136
185
|
}
|
|
@@ -138,12 +187,23 @@ export const MediaThumbnail = memo(function MediaThumbnail({
|
|
|
138
187
|
|
|
139
188
|
return <FileIcon color={iconColor} style={iconStyles} />;
|
|
140
189
|
}
|
|
141
|
-
}, [
|
|
190
|
+
}, [
|
|
191
|
+
renderThumbnailIcon,
|
|
192
|
+
url,
|
|
193
|
+
contentType,
|
|
194
|
+
fileName,
|
|
195
|
+
dimensions,
|
|
196
|
+
size,
|
|
197
|
+
aspect,
|
|
198
|
+
iconColor,
|
|
199
|
+
iconStyles,
|
|
200
|
+
iconName,
|
|
201
|
+
]);
|
|
142
202
|
|
|
143
203
|
return (
|
|
144
204
|
<div
|
|
145
205
|
className={cx(
|
|
146
|
-
"w-full h-full justify-center flex items-center aspect-square",
|
|
206
|
+
"n-w-full n-h-full n-justify-center n-flex n-items-center n-aspect-square",
|
|
147
207
|
className
|
|
148
208
|
)}
|
|
149
209
|
style={{ backgroundColor }}
|
|
@@ -163,6 +223,16 @@ const iconSizeMap: Record<CollectionItemSize, number | string> = {
|
|
|
163
223
|
xl: "max(15px, 20%)",
|
|
164
224
|
};
|
|
165
225
|
|
|
226
|
+
const imageSizeMap: Record<CollectionItemSize, number | string> = {
|
|
227
|
+
xxs: 12 * 2,
|
|
228
|
+
xs: 18 * 2,
|
|
229
|
+
small: 24 * 2,
|
|
230
|
+
medium: 36 * 2,
|
|
231
|
+
mediumLarge: 48 * 2,
|
|
232
|
+
large: 64 * 2,
|
|
233
|
+
xl: 96 * 2,
|
|
234
|
+
};
|
|
235
|
+
|
|
166
236
|
function findFileIcon(fileName: string) {
|
|
167
237
|
const fileIcon = fileNames[fileName];
|
|
168
238
|
if (fileIcon) return fileIcon;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { MultiplayerUser } from "@noya-app/noya-multiplayer-react";
|
|
4
|
+
import { uuid } from "@noya-app/noya-utils";
|
|
2
5
|
import React, { forwardRef, memo, useMemo } from "react";
|
|
3
6
|
import { cssVars, INPUT_HEIGHT } from "../theme";
|
|
4
7
|
import { cx } from "../utils/classNames";
|
|
5
8
|
import { colorFromString } from "../utils/colorFromString";
|
|
6
9
|
import { Avatar } from "./Avatar";
|
|
7
10
|
import { Logo } from "./NoyaLogo";
|
|
8
|
-
import { Text } from "./Text";
|
|
9
11
|
|
|
10
12
|
export type MessageProps = {
|
|
11
13
|
role: "user" | "assistant" | "system" | "data";
|
|
@@ -20,7 +22,7 @@ export const Message = memo(
|
|
|
20
22
|
{ role, children, user, avatarSize = INPUT_HEIGHT }: MessageProps,
|
|
21
23
|
ref: React.ForwardedRef<HTMLDivElement>
|
|
22
24
|
) {
|
|
23
|
-
const randomId =
|
|
25
|
+
const randomId = uuid();
|
|
24
26
|
const userMessageBackgroundColor = colorFromString(user?.id ?? randomId);
|
|
25
27
|
const styles = useMemo(
|
|
26
28
|
() =>
|
|
@@ -34,8 +36,8 @@ export const Message = memo(
|
|
|
34
36
|
return (
|
|
35
37
|
<div
|
|
36
38
|
className={cx(
|
|
37
|
-
"flex gap-2",
|
|
38
|
-
role === "user" ? "flex-row-reverse" : "flex-row"
|
|
39
|
+
"n-flex n-gap-2",
|
|
40
|
+
role === "user" ? "n-flex-row-reverse" : "n-flex-row"
|
|
39
41
|
)}
|
|
40
42
|
>
|
|
41
43
|
{role === "assistant" ? (
|
|
@@ -56,18 +58,15 @@ export const Message = memo(
|
|
|
56
58
|
)}
|
|
57
59
|
<div
|
|
58
60
|
className={cx(
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
+
"markdown-message",
|
|
62
|
+
"n-flex n-flex-row n-gap-2 n-rounded-lg n-py-2 n-px-3 n-transition-all n-duration-200 n-max-w-[70%] n-text-sm",
|
|
63
|
+
role === "user" ? "n-text-white" : "n-text-text",
|
|
64
|
+
role === "user" ? "n-ml-auto" : "n-bg-input-background"
|
|
61
65
|
)}
|
|
62
66
|
style={styles}
|
|
63
67
|
ref={ref}
|
|
64
68
|
>
|
|
65
|
-
|
|
66
|
-
variant="small"
|
|
67
|
-
className={role === "user" ? "text-white" : "text-text"}
|
|
68
|
-
>
|
|
69
|
-
{children}
|
|
70
|
-
</Text>
|
|
69
|
+
{children}
|
|
71
70
|
</div>
|
|
72
71
|
</div>
|
|
73
72
|
);
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
Children,
|
|
5
|
+
ReactNode,
|
|
6
|
+
isValidElement,
|
|
7
|
+
useCallback,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
StackItem,
|
|
15
|
+
StackNavigator,
|
|
16
|
+
StackNavigatorProps,
|
|
17
|
+
} from "./StackNavigator";
|
|
18
|
+
|
|
19
|
+
export type NavigatorParams = Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
export type NavigatorRoute<Params extends NavigatorParams | null> = {
|
|
22
|
+
key: string;
|
|
23
|
+
name: string;
|
|
24
|
+
params: Params;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type NavigatorRoutesConfig<
|
|
28
|
+
RoutesConfig extends Record<string, NavigatorParams | null>,
|
|
29
|
+
> = {
|
|
30
|
+
[K in keyof RoutesConfig]: NavigatorRoute<RoutesConfig[K]>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type NavigatorPushFunction<
|
|
34
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
35
|
+
> = <K extends keyof Routes>(
|
|
36
|
+
...args: Routes[K]["params"] extends null
|
|
37
|
+
? [name: K]
|
|
38
|
+
: [name: K, params: Routes[K]["params"]]
|
|
39
|
+
) => void;
|
|
40
|
+
|
|
41
|
+
export type NavigatorReplaceFunction<
|
|
42
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
43
|
+
> = <K extends keyof Routes>(
|
|
44
|
+
...args: Routes[K]["params"] extends null
|
|
45
|
+
? [name: K]
|
|
46
|
+
: [name: K, params: Routes[K]["params"]]
|
|
47
|
+
) => void;
|
|
48
|
+
|
|
49
|
+
export type NavigatorNavigation<
|
|
50
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
51
|
+
> = {
|
|
52
|
+
push: NavigatorPushFunction<Routes>;
|
|
53
|
+
replace: NavigatorReplaceFunction<Routes>;
|
|
54
|
+
pop: () => void;
|
|
55
|
+
canGoBack: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type NavigatorRenderContext<
|
|
59
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
60
|
+
Name extends keyof Routes,
|
|
61
|
+
> = {
|
|
62
|
+
route: NavigatorRoute<Routes[Name]["params"]>;
|
|
63
|
+
navigation: NavigatorNavigation<Routes>;
|
|
64
|
+
stack: NavigatorStackEntry<Routes>[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type NavigatorHeaderProps<
|
|
68
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
69
|
+
Name extends keyof Routes,
|
|
70
|
+
> = NavigatorRenderContext<Routes, Name> & {
|
|
71
|
+
title: React.ReactNode;
|
|
72
|
+
right: React.ReactNode;
|
|
73
|
+
showBackButton: boolean;
|
|
74
|
+
onBack?: () => void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type NavigatorRenderHeader<
|
|
78
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
79
|
+
Name extends keyof Routes,
|
|
80
|
+
> =
|
|
81
|
+
| React.ReactNode
|
|
82
|
+
| ((context: NavigatorHeaderProps<Routes, Name>) => React.ReactNode);
|
|
83
|
+
|
|
84
|
+
export type NavigatorScreenProps<
|
|
85
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
86
|
+
Name extends keyof Routes,
|
|
87
|
+
> = {
|
|
88
|
+
name: Name;
|
|
89
|
+
title:
|
|
90
|
+
| React.ReactNode
|
|
91
|
+
| ((context: NavigatorRenderContext<Routes, Name>) => React.ReactNode);
|
|
92
|
+
right?:
|
|
93
|
+
| React.ReactNode
|
|
94
|
+
| ((context: NavigatorRenderContext<Routes, Name>) => React.ReactNode);
|
|
95
|
+
showBackButton?:
|
|
96
|
+
| boolean
|
|
97
|
+
| ((context: NavigatorRenderContext<Routes, Name>) => boolean);
|
|
98
|
+
renderHeader?: NavigatorRenderHeader<Routes, Name>;
|
|
99
|
+
children:
|
|
100
|
+
| React.ReactNode
|
|
101
|
+
| ((context: NavigatorRenderContext<Routes, Name>) => React.ReactNode);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const navigatorScreenIdentifier = Symbol("NavigatorScreen");
|
|
105
|
+
|
|
106
|
+
const NavigatorScreen = Object.assign(
|
|
107
|
+
<
|
|
108
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
109
|
+
Name extends keyof Routes,
|
|
110
|
+
>(
|
|
111
|
+
_props: NavigatorScreenProps<Routes, Name>
|
|
112
|
+
) => null,
|
|
113
|
+
{
|
|
114
|
+
[navigatorScreenIdentifier]: true as const,
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
type NavigatorScreenConfig<
|
|
119
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
120
|
+
Name extends keyof Routes,
|
|
121
|
+
> = NavigatorScreenProps<Routes, Name> & {
|
|
122
|
+
name: Name;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export type NavigatorStackEntry<
|
|
126
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
127
|
+
> = {
|
|
128
|
+
key: string;
|
|
129
|
+
name: keyof Routes;
|
|
130
|
+
params: Routes[keyof Routes]["params"];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export type NavigatorProps<
|
|
134
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
135
|
+
Name extends keyof Routes,
|
|
136
|
+
> = {
|
|
137
|
+
initialRoute: Routes[Name]["params"] extends null
|
|
138
|
+
? Name | { name: Name }
|
|
139
|
+
: { name: Name; params: Routes[Name]["params"] };
|
|
140
|
+
children: React.ReactNode;
|
|
141
|
+
showHeader?:
|
|
142
|
+
| boolean
|
|
143
|
+
| ((context: { stack: NavigatorStackEntry<Routes>[] }) => boolean);
|
|
144
|
+
renderHeader?: NavigatorRenderHeader<Routes, any>;
|
|
145
|
+
} & Pick<
|
|
146
|
+
StackNavigatorProps,
|
|
147
|
+
"className" | "style" | "contentClassName" | "cardStyle" | "cardClassName"
|
|
148
|
+
>;
|
|
149
|
+
|
|
150
|
+
type NavigatorScreenContextValue<
|
|
151
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
152
|
+
Name extends keyof Routes,
|
|
153
|
+
> = NavigatorRenderContext<Routes, Name>;
|
|
154
|
+
|
|
155
|
+
const NavigatorScreenContext = React.createContext<NavigatorScreenContextValue<
|
|
156
|
+
any,
|
|
157
|
+
any
|
|
158
|
+
> | null>(null);
|
|
159
|
+
|
|
160
|
+
export function useNavigator<
|
|
161
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
162
|
+
>(): NavigatorNavigation<Routes> {
|
|
163
|
+
const context = React.useContext(NavigatorScreenContext);
|
|
164
|
+
|
|
165
|
+
if (!context) {
|
|
166
|
+
throw new Error("useNavigator must be used within a Navigator screen");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return context.navigation;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function useNavigatorRoute<
|
|
173
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
174
|
+
Name extends keyof Routes,
|
|
175
|
+
>(): NavigatorRoute<Routes[Name]["params"]> {
|
|
176
|
+
const context = React.useContext(NavigatorScreenContext);
|
|
177
|
+
|
|
178
|
+
if (!context) {
|
|
179
|
+
throw new Error("useNavigatorRoute must be used within a Navigator screen");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return context.route;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isNavigatorScreenElement(
|
|
186
|
+
child: ReactNode
|
|
187
|
+
): child is React.ReactElement<NavigatorScreenProps<any, any>> {
|
|
188
|
+
return (
|
|
189
|
+
isValidElement(child) &&
|
|
190
|
+
Boolean(
|
|
191
|
+
(child.type as { [navigatorScreenIdentifier]?: boolean })?.[
|
|
192
|
+
navigatorScreenIdentifier
|
|
193
|
+
]
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function evaluateOption<T, C>(
|
|
199
|
+
option: T | ((context: C) => T) | undefined,
|
|
200
|
+
context: C
|
|
201
|
+
): T | undefined {
|
|
202
|
+
if (option instanceof Function) {
|
|
203
|
+
return option(context);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return option;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function NavigatorImpl<
|
|
210
|
+
Routes extends Record<string, NavigatorRoute<any>>,
|
|
211
|
+
Name extends keyof Routes,
|
|
212
|
+
>({
|
|
213
|
+
initialRoute,
|
|
214
|
+
children,
|
|
215
|
+
className,
|
|
216
|
+
style,
|
|
217
|
+
contentClassName,
|
|
218
|
+
cardStyle,
|
|
219
|
+
cardClassName,
|
|
220
|
+
showHeader,
|
|
221
|
+
renderHeader,
|
|
222
|
+
}: NavigatorProps<Routes, Name>) {
|
|
223
|
+
const screens = useMemo<NavigatorScreenConfig<Routes, any>[]>(() => {
|
|
224
|
+
const configs: NavigatorScreenConfig<Routes, any>[] = [];
|
|
225
|
+
|
|
226
|
+
Children.forEach(children, (child) => {
|
|
227
|
+
if (!isNavigatorScreenElement(child)) return;
|
|
228
|
+
|
|
229
|
+
configs.push(child.props as NavigatorScreenConfig<Routes, any>);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return configs;
|
|
233
|
+
}, [children]);
|
|
234
|
+
|
|
235
|
+
const screenMap = useMemo(() => {
|
|
236
|
+
const map = new Map<keyof Routes, NavigatorScreenConfig<Routes, any>>();
|
|
237
|
+
|
|
238
|
+
for (const screen of screens) {
|
|
239
|
+
if (map.has(screen.name)) {
|
|
240
|
+
console.warn(
|
|
241
|
+
`Navigator: duplicate screen name "${String(screen.name)}" detected. The latter definition will be used.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
map.set(screen.name, screen);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return map;
|
|
249
|
+
}, [screens]);
|
|
250
|
+
|
|
251
|
+
const keyRef = useRef(0);
|
|
252
|
+
const createEntry = useCallback(
|
|
253
|
+
<K extends keyof Routes>(
|
|
254
|
+
name: K,
|
|
255
|
+
params: Routes[K]["params"]
|
|
256
|
+
): NavigatorStackEntry<Routes> => ({
|
|
257
|
+
key: `${String(name)}-${keyRef.current++}`,
|
|
258
|
+
name,
|
|
259
|
+
params,
|
|
260
|
+
}),
|
|
261
|
+
[]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const initialScreenName =
|
|
265
|
+
typeof initialRoute === "string"
|
|
266
|
+
? initialRoute
|
|
267
|
+
: (initialRoute as { name: Name }).name;
|
|
268
|
+
|
|
269
|
+
if (!screenMap.has(initialScreenName)) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Navigator: initial route "${String(initialScreenName)}" is not registered as a screen.`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const [stack, setStack] = useState<NavigatorStackEntry<Routes>[]>(() => [
|
|
276
|
+
createEntry(
|
|
277
|
+
initialScreenName,
|
|
278
|
+
(initialRoute as { params: Routes[Name]["params"] }).params
|
|
279
|
+
),
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
const push = useCallback(
|
|
283
|
+
<K extends keyof Routes>(
|
|
284
|
+
...args: Routes[K]["params"] extends null
|
|
285
|
+
? [name: K]
|
|
286
|
+
: [name: K, params: Routes[K]["params"]]
|
|
287
|
+
) => {
|
|
288
|
+
const [name, params] = args;
|
|
289
|
+
|
|
290
|
+
const screen = screenMap.get(name);
|
|
291
|
+
|
|
292
|
+
if (!screen) {
|
|
293
|
+
console.warn(
|
|
294
|
+
`Navigator: attempted to push unknown screen "${String(name)}".`
|
|
295
|
+
);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setStack((prev) => [...prev, createEntry(name, params)]);
|
|
300
|
+
},
|
|
301
|
+
[createEntry, screenMap]
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const pop = useCallback(() => {
|
|
305
|
+
setStack((prev) => {
|
|
306
|
+
if (prev.length <= 1) return prev;
|
|
307
|
+
|
|
308
|
+
const nextStack = prev.slice(0, -1);
|
|
309
|
+
|
|
310
|
+
if (nextStack.length === 0) {
|
|
311
|
+
return nextStack;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const updatedStack = [...nextStack];
|
|
315
|
+
const topEntry = updatedStack[updatedStack.length - 1];
|
|
316
|
+
|
|
317
|
+
updatedStack[updatedStack.length - 1] = createEntry(
|
|
318
|
+
topEntry.name as keyof Routes,
|
|
319
|
+
topEntry.params as Routes[keyof Routes]["params"]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
return updatedStack;
|
|
323
|
+
});
|
|
324
|
+
}, [createEntry]);
|
|
325
|
+
|
|
326
|
+
const replace = useCallback(
|
|
327
|
+
<K extends keyof Routes>(
|
|
328
|
+
targetKey: string,
|
|
329
|
+
name: K,
|
|
330
|
+
params: Routes[K]["params"] | null
|
|
331
|
+
) => {
|
|
332
|
+
const screen = screenMap.get(name);
|
|
333
|
+
|
|
334
|
+
if (!screen) {
|
|
335
|
+
console.warn(
|
|
336
|
+
`Navigator: attempted to replace with unknown screen "${String(name)}".`
|
|
337
|
+
);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
setStack((prev) => {
|
|
342
|
+
const index = prev.findIndex((entry) => entry.key === targetKey);
|
|
343
|
+
|
|
344
|
+
if (index === -1) return prev;
|
|
345
|
+
|
|
346
|
+
const next = [...prev];
|
|
347
|
+
next[index] = createEntry(name, params as Routes[K]["params"]);
|
|
348
|
+
|
|
349
|
+
if (next.length > 0) {
|
|
350
|
+
const top = next[next.length - 1];
|
|
351
|
+
next[next.length - 1] = createEntry(
|
|
352
|
+
top.name as keyof Routes,
|
|
353
|
+
top.params as Routes[keyof Routes]["params"]
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return next;
|
|
358
|
+
});
|
|
359
|
+
},
|
|
360
|
+
[createEntry, screenMap]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const items = useMemo<StackItem[]>(() => {
|
|
364
|
+
return stack.flatMap((entry) => {
|
|
365
|
+
const screen = screenMap.get(entry.name);
|
|
366
|
+
|
|
367
|
+
if (!screen) return [];
|
|
368
|
+
|
|
369
|
+
const canGoBack = stack.length > 1;
|
|
370
|
+
|
|
371
|
+
const route: NavigatorRoute<Routes[keyof Routes]["params"]> = {
|
|
372
|
+
key: entry.key,
|
|
373
|
+
name: String(entry.name),
|
|
374
|
+
params: (entry.params ?? null) as Routes[keyof Routes]["params"],
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const navigation: NavigatorNavigation<Routes> = {
|
|
378
|
+
push,
|
|
379
|
+
replace: (...args) => replace(entry.key, args[0], args[1]),
|
|
380
|
+
pop,
|
|
381
|
+
canGoBack,
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const context: NavigatorRenderContext<Routes, any> = {
|
|
385
|
+
route,
|
|
386
|
+
navigation,
|
|
387
|
+
stack,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const title = evaluateOption(screen.title, context);
|
|
391
|
+
const right = evaluateOption(screen.right, context);
|
|
392
|
+
const showBackButton = evaluateOption(screen.showBackButton, context);
|
|
393
|
+
const headerOption = screen.renderHeader ?? renderHeader;
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
key: entry.key,
|
|
397
|
+
title,
|
|
398
|
+
right,
|
|
399
|
+
showBackButton,
|
|
400
|
+
renderHeader:
|
|
401
|
+
headerOption === undefined
|
|
402
|
+
? undefined
|
|
403
|
+
: (headerProps) =>
|
|
404
|
+
evaluateOption(headerOption, {
|
|
405
|
+
...context,
|
|
406
|
+
title,
|
|
407
|
+
right,
|
|
408
|
+
showBackButton: headerProps.showBackButton,
|
|
409
|
+
onBack: headerProps.onBack,
|
|
410
|
+
}),
|
|
411
|
+
renderContent: () => {
|
|
412
|
+
return (
|
|
413
|
+
<NavigatorScreenContext.Provider value={context}>
|
|
414
|
+
{evaluateOption(screen.children, context)}
|
|
415
|
+
</NavigatorScreenContext.Provider>
|
|
416
|
+
);
|
|
417
|
+
},
|
|
418
|
+
} satisfies StackItem;
|
|
419
|
+
});
|
|
420
|
+
}, [pop, replace, screenMap, stack, push, renderHeader]);
|
|
421
|
+
|
|
422
|
+
const shouldShowHeader = evaluateOption(showHeader, {
|
|
423
|
+
stack,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<StackNavigator
|
|
428
|
+
items={items}
|
|
429
|
+
onBack={pop}
|
|
430
|
+
className={className}
|
|
431
|
+
style={style}
|
|
432
|
+
contentClassName={contentClassName}
|
|
433
|
+
cardStyle={cardStyle}
|
|
434
|
+
cardClassName={cardClassName}
|
|
435
|
+
showHeader={shouldShowHeader}
|
|
436
|
+
/>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export const Navigator = Object.assign(NavigatorImpl, {
|
|
441
|
+
Screen: NavigatorScreen,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
export function createNavigator<
|
|
445
|
+
RoutesConfig extends Record<string, NavigatorParams | null>,
|
|
446
|
+
>() {
|
|
447
|
+
return Object.assign(
|
|
448
|
+
NavigatorImpl as <Name extends keyof RoutesConfig>(
|
|
449
|
+
props: NavigatorProps<NavigatorRoutesConfig<RoutesConfig>, Name>
|
|
450
|
+
) => ReactNode,
|
|
451
|
+
{
|
|
452
|
+
Screen: NavigatorScreen as <Name extends keyof RoutesConfig>(
|
|
453
|
+
props: NavigatorScreenProps<NavigatorRoutesConfig<RoutesConfig>, Name>
|
|
454
|
+
) => ReactNode,
|
|
455
|
+
useNavigator: useNavigator<NavigatorRoutesConfig<RoutesConfig>>,
|
|
456
|
+
useNavigatorRoute: useNavigatorRoute as <Name extends keyof RoutesConfig>(
|
|
457
|
+
name: Name
|
|
458
|
+
) => NavigatorRoute<NavigatorRoutesConfig<RoutesConfig>[Name]["params"]>,
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
}
|
|
@@ -14,7 +14,7 @@ export const Logo = forwardRef(function Logo(
|
|
|
14
14
|
fill="none"
|
|
15
15
|
xmlns="http://www.w3.org/2000/svg"
|
|
16
16
|
{...props}
|
|
17
|
-
className="w-6 aspect-[24/26]"
|
|
17
|
+
className="n-w-6 n-aspect-[24/26]"
|
|
18
18
|
>
|
|
19
19
|
<path
|
|
20
20
|
d="M6.5 3H2V12H5V6H6.5C7.32843 6 8 6.67157 8 7.5V12H11V7.5C11 5.01472 8.98528 3 6.5 3Z"
|