@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.
Files changed (143) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +25 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +648 -333
  5. package/dist/index.d.ts +648 -333
  6. package/dist/index.js +13813 -9157
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +13949 -9269
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +10 -22
  11. package/src/__tests__/validateDropIndicator.test.ts +26 -41
  12. package/src/__tests__/workspaceLayout.test.ts +281 -0
  13. package/src/components/ActionMenu.tsx +2 -0
  14. package/src/components/ActivityIndicator.tsx +13 -6
  15. package/src/components/ActivityLog.tsx +197 -0
  16. package/src/components/AnimatePresence.tsx +43 -26
  17. package/src/components/Avatar.tsx +45 -26
  18. package/src/components/Banner.tsx +57 -11
  19. package/src/components/BaseToolbar.tsx +67 -34
  20. package/src/components/Breadcrumbs.tsx +4 -2
  21. package/src/components/Button.tsx +28 -23
  22. package/src/components/Checkbox.tsx +6 -35
  23. package/src/components/Chip.tsx +47 -33
  24. package/src/components/Collection.tsx +20 -12
  25. package/src/components/ColorSwatch.tsx +42 -22
  26. package/src/components/ColorSwatchControl.tsx +7 -5
  27. package/src/components/Combobox.tsx +8 -6
  28. package/src/components/ComboboxMenu.tsx +6 -5
  29. package/src/components/CommandPalette.tsx +3 -1
  30. package/src/components/ContextMenu.tsx +3 -1
  31. package/src/components/Dialog.tsx +9 -13
  32. package/src/components/DimensionInput.tsx +2 -0
  33. package/src/components/Divider.tsx +18 -14
  34. package/src/components/DraggableMenuButton.tsx +11 -5
  35. package/src/components/Drawer.tsx +14 -6
  36. package/src/components/DropdownMenu.tsx +4 -2
  37. package/src/components/EditableText.tsx +4 -2
  38. package/src/components/Fade.tsx +13 -13
  39. package/src/components/FileUploadIndicator.tsx +108 -0
  40. package/src/components/FloatingWindow.tsx +4 -2
  41. package/src/components/Grid.tsx +58 -42
  42. package/src/components/GridView.tsx +65 -36
  43. package/src/components/IVirtualizedList.tsx +5 -0
  44. package/src/components/IconButton.tsx +2 -0
  45. package/src/components/Icons.tsx +24 -4
  46. package/src/components/InputField.tsx +21 -19
  47. package/src/components/InspectorContainer.tsx +2 -2
  48. package/src/components/InspectorPrimitives.tsx +15 -9
  49. package/src/components/Label.tsx +4 -3
  50. package/src/components/LabeledElementView.tsx +9 -5
  51. package/src/components/LabeledField.tsx +8 -6
  52. package/src/components/List.tsx +112 -46
  53. package/src/components/ListMenu.tsx +91 -0
  54. package/src/components/ListNavigator.tsx +106 -0
  55. package/src/components/ListView.tsx +22 -1147
  56. package/src/components/MediaThumbnail.tsx +84 -14
  57. package/src/components/Message.tsx +11 -12
  58. package/src/components/Navigator.tsx +461 -0
  59. package/src/components/NoyaLogo.tsx +1 -1
  60. package/src/components/Popover.tsx +12 -6
  61. package/src/components/Progress.tsx +31 -8
  62. package/src/components/ResizableContainer.tsx +187 -0
  63. package/src/components/RingProgress.tsx +128 -0
  64. package/src/components/ScrollArea.tsx +7 -5
  65. package/src/components/ScrollableSidebar.tsx +10 -0
  66. package/src/components/SearchCompletionMenu.tsx +5 -3
  67. package/src/components/Section.tsx +109 -57
  68. package/src/components/SegmentedControl.tsx +51 -25
  69. package/src/components/SelectMenu.tsx +16 -21
  70. package/src/components/SelectionToolbar.tsx +7 -4
  71. package/src/components/Slider.tsx +12 -10
  72. package/src/components/Spacer.tsx +2 -2
  73. package/src/components/StackNavigator.tsx +378 -0
  74. package/src/components/Stepper.tsx +88 -0
  75. package/src/components/Switch.tsx +7 -5
  76. package/src/components/Tabs.tsx +6 -4
  77. package/src/components/Text.tsx +29 -18
  78. package/src/components/TextArea.tsx +5 -3
  79. package/src/components/Toast.tsx +5 -5
  80. package/src/components/Toolbar.tsx +57 -10
  81. package/src/components/ToolbarDrawer.tsx +29 -0
  82. package/src/components/Tooltip.tsx +2 -2
  83. package/src/components/TreeView.tsx +13 -4
  84. package/src/components/UserPointer.tsx +6 -4
  85. package/src/components/ai-assistant/AIAssistantLayout.tsx +10 -8
  86. package/src/components/blocks/ImageBlockComponent.tsx +162 -0
  87. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +6 -4
  88. package/src/components/file-explorer/FileExplorerLayout.tsx +22 -8
  89. package/src/components/internal/Checkmark.tsx +24 -0
  90. package/src/components/internal/Menu.tsx +24 -19
  91. package/src/components/internal/MenuViewport.tsx +15 -6
  92. package/src/components/internal/SelectItem.tsx +7 -26
  93. package/src/components/internal/TextInput.tsx +7 -1
  94. package/src/components/listView/ListViewContexts.tsx +68 -0
  95. package/src/components/listView/ListViewEditableRowTitle.tsx +55 -0
  96. package/src/components/listView/ListViewRoot.tsx +521 -0
  97. package/src/components/listView/ListViewRow.tsx +475 -0
  98. package/src/components/listView/ListViewRowTitle.tsx +21 -0
  99. package/src/components/listView/types.ts +11 -0
  100. package/src/components/pipeline/PipelineResultLayout.tsx +6 -6
  101. package/src/components/sorting/DragRegistration.tsx +10 -5
  102. package/src/components/sorting/SharedDragProvider.tsx +134 -27
  103. package/src/components/sorting/Sortable.tsx +90 -27
  104. package/src/components/sorting/createSharedDrag.tsx +7 -6
  105. package/src/components/sorting/sorting.ts +6 -9
  106. package/src/components/workspace/DrawerWorkspaceLayout.tsx +110 -29
  107. package/src/components/workspace/PanelWorkspaceLayout.tsx +148 -99
  108. package/src/components/workspace/VerticalTabMenu.tsx +46 -0
  109. package/src/components/workspace/WorkspaceLayout.tsx +76 -63
  110. package/src/components/workspace/constants.ts +4 -0
  111. package/src/components/workspace/panelStorage.ts +216 -0
  112. package/src/components/workspace/types.ts +11 -4
  113. package/src/contexts/DesignSystemConfiguration.tsx +12 -5
  114. package/src/contexts/DialogContext.tsx +19 -5
  115. package/src/contexts/FloatingWindowContext.tsx +2 -0
  116. package/src/contexts/GlobalInputBlurContext.tsx +2 -0
  117. package/src/contexts/ImageDataContext.tsx +2 -0
  118. package/src/contexts/LinkComponentContext.tsx +17 -0
  119. package/src/contexts/OpenPortalsContext.tsx +2 -0
  120. package/src/contexts/PortalScopeContext.tsx +2 -0
  121. package/src/hooks/mergeEventHandlers.ts +1 -1
  122. package/src/hooks/useHover.ts +30 -17
  123. package/src/hooks/useIndent.ts +5 -8
  124. package/src/hooks/useLabel.ts +2 -0
  125. package/src/hooks/useObjectURL.ts +5 -3
  126. package/src/hooks/usePlatform.ts +5 -3
  127. package/src/hooks/useTheme.ts +3 -1
  128. package/src/index.css +43 -10
  129. package/src/index.tsx +15 -7
  130. package/src/utils/classNames.ts +56 -9
  131. package/src/utils/combobox.ts +2 -0
  132. package/src/utils/editableBlockStyles.ts +2 -2
  133. package/src/utils/formatByteSize.ts +9 -0
  134. package/src/utils/inputs.ts +8 -6
  135. package/src/utils/sketchColor.ts +0 -34
  136. package/src/utils/withSeparatorElements.ts +1 -1
  137. package/tailwind.config.ts +1 -0
  138. package/src/components/FillInputField.tsx +0 -37
  139. package/src/components/FillPreviewBackground.tsx +0 -132
  140. package/src/components/GradientPicker.tsx +0 -88
  141. package/src/hooks/usePreservePanelSize.tsx +0 -183
  142. package/src/mediaQuery.ts +0 -13
  143. 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 MediaThumbnailProps = {
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
- if (!url) return <ImageIcon color={iconColor} style={iconStyles} />;
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
- }, [contentType, url, iconColor, iconStyles, iconName, fileName]);
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 = crypto.randomUUID();
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
- "flex flex-row gap-2 rounded-lg py-2 px-3 transition-all duration-200 min-w-6 max-w-[70%] items-start",
60
- role === "user" ? "ml-auto" : "bg-input-background"
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
- <Text
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"