@noya-app/noya-designsystem 0.1.64 → 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 +12 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +501 -228
- package/dist/index.d.ts +501 -228
- package/dist/index.js +6738 -2571
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6697 -2499
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/components/ActivityIndicator.tsx +9 -4
- package/src/components/ActivityLog.tsx +197 -0
- package/src/components/AnimatePresence.tsx +37 -22
- package/src/components/Avatar.tsx +42 -25
- package/src/components/Banner.tsx +57 -11
- package/src/components/BaseToolbar.tsx +29 -7
- package/src/components/Breadcrumbs.tsx +2 -2
- package/src/components/Button.tsx +23 -24
- package/src/components/Checkbox.tsx +6 -35
- package/src/components/Chip.tsx +45 -33
- package/src/components/Collection.tsx +16 -11
- package/src/components/ColorSwatch.tsx +3 -3
- package/src/components/ColorSwatchControl.tsx +2 -2
- package/src/components/Combobox.tsx +6 -6
- package/src/components/ComboboxMenu.tsx +6 -5
- package/src/components/CommandPalette.tsx +1 -1
- package/src/components/Dialog.tsx +6 -12
- package/src/components/Divider.tsx +8 -8
- package/src/components/DraggableMenuButton.tsx +9 -5
- package/src/components/Drawer.tsx +5 -5
- package/src/components/DropdownMenu.tsx +1 -1
- package/src/components/EditableText.tsx +2 -2
- package/src/components/Fade.tsx +13 -13
- package/src/components/FileUploadIndicator.tsx +11 -11
- package/src/components/FloatingWindow.tsx +2 -2
- package/src/components/Grid.tsx +18 -11
- package/src/components/GridView.tsx +32 -25
- package/src/components/IVirtualizedList.tsx +5 -0
- package/src/components/Icons.tsx +24 -4
- package/src/components/InputField.tsx +19 -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 +3 -3
- package/src/components/LabeledField.tsx +6 -6
- package/src/components/List.tsx +103 -45
- package/src/components/ListMenu.tsx +91 -0
- package/src/components/ListNavigator.tsx +106 -0
- package/src/components/ListView.tsx +20 -1158
- package/src/components/MediaThumbnail.tsx +56 -15
- package/src/components/Message.tsx +5 -5
- package/src/components/Navigator.tsx +461 -0
- package/src/components/NoyaLogo.tsx +1 -1
- package/src/components/Popover.tsx +9 -5
- package/src/components/Progress.tsx +19 -9
- package/src/components/ResizableContainer.tsx +187 -0
- package/src/components/RingProgress.tsx +128 -0
- package/src/components/ScrollArea.tsx +4 -4
- package/src/components/ScrollableSidebar.tsx +10 -0
- package/src/components/SearchCompletionMenu.tsx +3 -3
- package/src/components/Section.tsx +107 -57
- package/src/components/SegmentedControl.tsx +29 -29
- package/src/components/SelectMenu.tsx +13 -20
- package/src/components/SelectionToolbar.tsx +4 -3
- package/src/components/Slider.tsx +9 -9
- 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 +4 -4
- package/src/components/Tabs.tsx +3 -3
- package/src/components/Text.tsx +23 -13
- package/src/components/TextArea.tsx +3 -3
- package/src/components/Toast.tsx +4 -4
- package/src/components/Toolbar.tsx +50 -9
- package/src/components/ToolbarDrawer.tsx +2 -2
- package/src/components/Tooltip.tsx +1 -1
- package/src/components/TreeView.tsx +11 -4
- package/src/components/UserPointer.tsx +4 -4
- package/src/components/ai-assistant/AIAssistantLayout.tsx +8 -8
- package/src/components/blocks/ImageBlockComponent.tsx +13 -7
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +4 -4
- package/src/components/file-explorer/FileExplorerLayout.tsx +20 -8
- package/src/components/internal/Checkmark.tsx +1 -1
- package/src/components/internal/Menu.tsx +22 -18
- package/src/components/internal/MenuViewport.tsx +13 -6
- package/src/components/internal/SelectItem.tsx +3 -3
- package/src/components/internal/TextInput.tsx +5 -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 +2 -1
- package/src/components/sorting/SharedDragProvider.tsx +91 -9
- package/src/components/sorting/Sortable.tsx +71 -17
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +5 -5
- package/src/components/workspace/PanelWorkspaceLayout.tsx +65 -49
- package/src/components/workspace/VerticalTabMenu.tsx +25 -15
- package/src/components/workspace/WorkspaceLayout.tsx +30 -21
- package/src/contexts/DialogContext.tsx +17 -5
- package/src/hooks/useIndent.ts +3 -8
- package/src/index.css +24 -10
- package/src/index.tsx +10 -5
- package/src/utils/classNames.ts +52 -6
- package/src/utils/editableBlockStyles.ts +2 -2
- package/src/utils/formatByteSize.ts +1 -0
- package/src/utils/inputs.ts +8 -6
- package/src/utils/sketchColor.ts +0 -34
- package/tailwind.config.ts +1 -0
- package/src/components/FillInputField.tsx +0 -37
- package/src/components/FillPreviewBackground.tsx +0 -134
- package/src/components/GradientPicker.tsx +0 -90
- package/src/utils/getGradientBackground.tsx +0 -31
|
@@ -75,60 +75,60 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
|
75
75
|
value={(value ?? "").toString()}
|
|
76
76
|
disabled={disabled}
|
|
77
77
|
className={cx(
|
|
78
|
-
"group",
|
|
79
|
-
"flex items-center justify-center",
|
|
80
|
-
"font-sans text-button relative flex-1 appearance-none focus:outline-none transition-colors",
|
|
78
|
+
"n-group",
|
|
79
|
+
"n-flex n-items-center n-justify-center",
|
|
80
|
+
"n-font-sans n-text-button n-relative n-flex-1 n-appearance-none focus:n-outline-none n-transition-colors",
|
|
81
81
|
// text color
|
|
82
82
|
variant === "buttons"
|
|
83
|
-
? "text-button-text"
|
|
84
|
-
: "text-segmented-control-item",
|
|
83
|
+
? "n-text-button-text"
|
|
84
|
+
: "n-text-segmented-control-item",
|
|
85
85
|
// font weight
|
|
86
86
|
variant === "tabs"
|
|
87
|
-
? "font-semibold"
|
|
87
|
+
? "n-font-semibold"
|
|
88
88
|
: variant === "buttons"
|
|
89
|
-
? "font-medium"
|
|
90
|
-
: "font-normal",
|
|
89
|
+
? "n-font-medium"
|
|
90
|
+
: "n-font-normal",
|
|
91
91
|
// focus ring
|
|
92
92
|
variant === "tabs"
|
|
93
93
|
? ""
|
|
94
94
|
: variant === "buttons"
|
|
95
|
-
? "focus-visible:ring-2 focus-visible:ring-primary focus:shadow-[0_0_0_1px_var(--n-popover-background)_inset] transition-all"
|
|
95
|
+
? "focus-visible:n-ring-2 focus-visible:n-ring-primary focus:n-shadow-[0_0_0_1px_var(--n-popover-background)_inset] n-transition-all"
|
|
96
96
|
: colorScheme === "secondary"
|
|
97
|
-
? "focus:ring-2 focus:ring-secondary focus:ring-offset-1"
|
|
98
|
-
: "focus:ring-2 focus:ring-primary focus:ring-offset-1",
|
|
97
|
+
? "focus:n-ring-2 focus:n-ring-secondary focus:n-ring-offset-1"
|
|
98
|
+
: "focus:n-ring-2 focus:n-ring-primary focus:n-ring-offset-1",
|
|
99
99
|
// checked
|
|
100
100
|
variant === "tabs"
|
|
101
|
-
? "aria-checked:text-primary"
|
|
101
|
+
? "aria-checked:n-text-primary"
|
|
102
102
|
: variant === "buttons"
|
|
103
|
-
? "active:text-button-text-active active:bg-button-background-active aria-checked:text-button-text-active aria-checked:bg-button-background-active"
|
|
103
|
+
? "active:n-text-button-text-active active:n-bg-button-background-active aria-checked:n-text-button-text-active aria-checked:n-bg-button-background-active"
|
|
104
104
|
: colorScheme === "secondary"
|
|
105
|
-
? "aria-checked:bg-secondary aria-checked:text-white"
|
|
105
|
+
? "aria-checked:n-bg-secondary aria-checked:n-text-white"
|
|
106
106
|
: colorScheme === "primary"
|
|
107
|
-
? "aria-checked:bg-primary aria-checked:text-white"
|
|
108
|
-
: "aria-checked:bg-segmented-control-item-active-background aria-checked:text-segmented-control-item-active-text",
|
|
109
|
-
"focus:z-interactable",
|
|
107
|
+
? "aria-checked:n-bg-primary aria-checked:n-text-white"
|
|
108
|
+
: "aria-checked:n-bg-segmented-control-item-active-background aria-checked:n-text-segmented-control-item-active-text",
|
|
109
|
+
"focus:n-z-interactable",
|
|
110
110
|
// checked default (all colorSchemes)
|
|
111
111
|
variant === "default" &&
|
|
112
|
-
"aria-checked:border-r-transparent [&:has(+[aria-checked=true])]:border-r-transparent aria-checked:shadow-segment",
|
|
112
|
+
"aria-checked:n-border-r-transparent [&:has(+[aria-checked=true])]:n-border-r-transparent aria-checked:n-shadow-segment",
|
|
113
113
|
// hover
|
|
114
|
-
variant === "buttons" && "hover:bg-button-background-hover",
|
|
114
|
+
variant === "buttons" && "hover:n-bg-button-background-hover",
|
|
115
115
|
// spacing and borders
|
|
116
116
|
variant === "default"
|
|
117
|
-
? "px-1 rounded border-r border-divider last:border-r-0"
|
|
117
|
+
? "n-px-1 n-rounded n-border-r n-border-divider last:n-border-r-0"
|
|
118
118
|
: variant === "buttons"
|
|
119
|
-
? "px-1 rounded-sm max-w-fit"
|
|
120
|
-
: "px-1.5 border-y-2 border-y-transparent aria-checked:border-b-primary",
|
|
119
|
+
? "n-px-1 n-rounded-sm n-max-w-fit"
|
|
120
|
+
: "n-px-1.5 n-border-y-2 n-border-y-transparent aria-checked:n-border-b-primary",
|
|
121
121
|
// icon only buttons
|
|
122
|
-
variant === "buttons" && title === "" && icon && "min-w-input-height",
|
|
122
|
+
variant === "buttons" && title === "" && icon && "n-min-w-input-height",
|
|
123
123
|
className
|
|
124
124
|
)}
|
|
125
125
|
{...props}
|
|
126
126
|
>
|
|
127
127
|
<span
|
|
128
128
|
className={cx(
|
|
129
|
-
"inline-flex whitespace-pre flex-nowrap gap-1.5 items-center justify-center align-middle text-center",
|
|
129
|
+
"n-inline-flex n-whitespace-pre n-flex-nowrap n-gap-1.5 n-items-center n-justify-center n-align-middle n-text-center",
|
|
130
130
|
variant === "tabs" &&
|
|
131
|
-
"group-focus-visible:ring-2 group-focus-visible:ring-primary-pastel group-focus-visible:ring-offset-2"
|
|
131
|
+
"group-focus-visible:n-ring-2 group-focus-visible:n-ring-primary-pastel group-focus-visible:n-ring-offset-2"
|
|
132
132
|
)}
|
|
133
133
|
>
|
|
134
134
|
{icon && renderIcon(icon)}
|
|
@@ -176,11 +176,11 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
176
176
|
value={value}
|
|
177
177
|
onValueChange={handleValueChange}
|
|
178
178
|
className={cx(
|
|
179
|
-
`appearance-none relative outline-none`,
|
|
179
|
+
`n-appearance-none n-relative n-outline-none`,
|
|
180
180
|
variant === "default" &&
|
|
181
|
-
"grid min-h-input-height bg-input-background rounded py-0.5 px-0.5",
|
|
182
|
-
variant === "tabs" && "flex gap-1.5 min-h-[33px]",
|
|
183
|
-
variant === "buttons" && "flex gap-1.5 min-h-input-height",
|
|
181
|
+
"n-grid n-min-h-input-height n-bg-input-background n-rounded n-py-0.5 n-px-0.5",
|
|
182
|
+
variant === "tabs" && "n-flex n-gap-1.5 n-min-h-[33px]",
|
|
183
|
+
variant === "buttons" && "n-flex n-gap-1.5 n-min-h-input-height",
|
|
184
184
|
className
|
|
185
185
|
)}
|
|
186
186
|
style={{
|
|
@@ -54,15 +54,8 @@ const readOnlyStyle: CSSProperties = {
|
|
|
54
54
|
textAlign: "left",
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
const scrollButtonStyles =
|
|
58
|
-
flex
|
|
59
|
-
items-center
|
|
60
|
-
justify-center
|
|
61
|
-
h-[25px]
|
|
62
|
-
cursor-default
|
|
63
|
-
text-text-muted
|
|
64
|
-
hover:text-text
|
|
65
|
-
`;
|
|
57
|
+
const scrollButtonStyles =
|
|
58
|
+
"n-flex n-items-center n-justify-center n-h-[25px] n-cursor-default n-text-text-muted hover:n-text-text";
|
|
66
59
|
|
|
67
60
|
const insetEndStyles = getInsetEndStyles();
|
|
68
61
|
|
|
@@ -102,27 +95,27 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
|
|
|
102
95
|
id={id}
|
|
103
96
|
style={style}
|
|
104
97
|
className={cx(
|
|
105
|
-
className ?? "flex w-full",
|
|
106
|
-
"flex-1 focus:z-interactable relative",
|
|
107
|
-
isOpen && "ring-2 ring-primary"
|
|
98
|
+
className ?? "n-flex n-w-full",
|
|
99
|
+
"n-flex-1 focus:n-z-interactable n-relative",
|
|
100
|
+
isOpen && "n-ring-2 n-ring-primary"
|
|
108
101
|
)}
|
|
109
102
|
disabled={disabled}
|
|
110
103
|
>
|
|
111
|
-
{icon && <span className="pr-1.5">{renderIcon(icon)}</span>}
|
|
104
|
+
{icon && <span className="n-pr-1.5">{renderIcon(icon)}</span>}
|
|
112
105
|
{label && labelType === "inset" && (
|
|
113
106
|
<div className={insetEndStyles}>
|
|
114
|
-
<Label className="pr-4 text-text-disabled" htmlFor={id}>
|
|
107
|
+
<Label className="n-pr-4 n-text-text-disabled" htmlFor={id}>
|
|
115
108
|
{label}
|
|
116
109
|
</Label>
|
|
117
110
|
</div>
|
|
118
111
|
)}
|
|
119
|
-
<span className="flex-1 flex">
|
|
112
|
+
<span className="n-flex-1 n-flex">
|
|
120
113
|
<Select.Value placeholder={placeholder} />
|
|
121
114
|
</span>
|
|
122
115
|
<DropdownChevronIcon
|
|
123
116
|
className={cx(
|
|
124
|
-
"transition-transform duration-100 ml-1.5",
|
|
125
|
-
isOpen ? "rotate-180" : "rotate-0"
|
|
117
|
+
"n-transition-transform n-duration-100 n-ml-1.5",
|
|
118
|
+
isOpen ? "n-rotate-180" : "n-rotate-0"
|
|
126
119
|
)}
|
|
127
120
|
/>
|
|
128
121
|
</Button>
|
|
@@ -181,11 +174,11 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
181
174
|
id={id}
|
|
182
175
|
style={style}
|
|
183
176
|
contentStyle={readOnlyStyle}
|
|
184
|
-
className={cx(
|
|
177
|
+
className={cx("n-flex-1 focus:n-z-interactable n-gap-1.5", className)}
|
|
185
178
|
disabled={disabled}
|
|
186
179
|
>
|
|
187
180
|
{icon && renderIcon(icon)}
|
|
188
|
-
<span className="flex flex-1">{selectedItem?.title ?? value}</span>
|
|
181
|
+
<span className="n-flex n-flex-1">{selectedItem?.title ?? value}</span>
|
|
189
182
|
</Button>
|
|
190
183
|
),
|
|
191
184
|
[icon, id, style, className, disabled, value, selectedItem]
|
|
@@ -231,7 +224,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
231
224
|
>
|
|
232
225
|
<Select.ScrollUpButton className={scrollButtonStyles}>
|
|
233
226
|
<ChevronUpIcon
|
|
234
|
-
className={cx("transition-transform duration-100")}
|
|
227
|
+
className={cx("n-transition-transform n-duration-100")}
|
|
235
228
|
/>
|
|
236
229
|
</Select.ScrollUpButton>
|
|
237
230
|
<Select.Viewport>
|
|
@@ -34,7 +34,7 @@ type PopperPrimitiveContentProps = ComponentProps<
|
|
|
34
34
|
|
|
35
35
|
type SelectionToolbarContainerProps = Pick<
|
|
36
36
|
PopperPrimitiveContentProps,
|
|
37
|
-
"side" | "sideOffset" | "align"
|
|
37
|
+
"side" | "sideOffset" | "align" | "avoidCollisions"
|
|
38
38
|
> & {
|
|
39
39
|
rect: Rect;
|
|
40
40
|
children: React.ReactNode;
|
|
@@ -52,6 +52,7 @@ export const SelectionToolbarContainer = memo(
|
|
|
52
52
|
side = "top",
|
|
53
53
|
sideOffset = 10,
|
|
54
54
|
align = "center",
|
|
55
|
+
avoidCollisions = true,
|
|
55
56
|
containerClassName,
|
|
56
57
|
containerStyle,
|
|
57
58
|
transitionDuration = 0.2,
|
|
@@ -74,7 +75,7 @@ export const SelectionToolbarContainer = memo(
|
|
|
74
75
|
side={side}
|
|
75
76
|
sideOffset={sideOffset}
|
|
76
77
|
align={align}
|
|
77
|
-
avoidCollisions
|
|
78
|
+
avoidCollisions={avoidCollisions}
|
|
78
79
|
collisionPadding={10}
|
|
79
80
|
contentEditable={false}
|
|
80
81
|
style={{
|
|
@@ -88,7 +89,7 @@ export const SelectionToolbarContainer = memo(
|
|
|
88
89
|
<div
|
|
89
90
|
ref={containerRef}
|
|
90
91
|
className={cx(
|
|
91
|
-
"flex gap-1 bg-popover-background rounded shadow-popover p-1",
|
|
92
|
+
"n-flex n-gap-1 n-bg-popover-background n-rounded n-shadow-popover n-p-1",
|
|
92
93
|
containerClassName
|
|
93
94
|
)}
|
|
94
95
|
style={containerStyle}
|
|
@@ -70,7 +70,7 @@ export const Slider = memo(function Slider({
|
|
|
70
70
|
value={arrayValue}
|
|
71
71
|
onValueChange={handleValueChange}
|
|
72
72
|
className={cx(
|
|
73
|
-
"flex relative items-center select-none touch-none h-input-height rounded flex-grow max-h-input-height cursor-pointer",
|
|
73
|
+
"n-flex n-relative n-items-center n-select-none n-touch-none n-h-input-height n-rounded n-flex-grow n-max-h-input-height n-cursor-pointer",
|
|
74
74
|
className
|
|
75
75
|
)}
|
|
76
76
|
style={style}
|
|
@@ -78,16 +78,16 @@ export const Slider = memo(function Slider({
|
|
|
78
78
|
onBlur={onBlur}
|
|
79
79
|
disabled={readOnly}
|
|
80
80
|
>
|
|
81
|
-
<div className="relative flex-grow h-[3px] overflow-hidden">
|
|
81
|
+
<div className="n-relative n-flex-grow n-h-[3px] n-overflow-hidden">
|
|
82
82
|
<div
|
|
83
|
-
className="absolute h-full rounded-l bg-slider-active-background"
|
|
83
|
+
className="n-absolute n-h-full n-rounded-l n-bg-slider-active-background"
|
|
84
84
|
style={{
|
|
85
85
|
left: 0,
|
|
86
86
|
width: `calc(${ratio * 100}% - ${ratio * 8}px - 2px)`,
|
|
87
87
|
}}
|
|
88
88
|
/>
|
|
89
89
|
<div
|
|
90
|
-
className="absolute h-full rounded-r bg-input-background"
|
|
90
|
+
className="n-absolute n-h-full n-rounded-r n-bg-input-background"
|
|
91
91
|
style={{
|
|
92
92
|
right: 0,
|
|
93
93
|
width: `calc(100% - ${ratio * 100}% + ${ratio * 8}px - 10px)`,
|
|
@@ -97,12 +97,12 @@ export const Slider = memo(function Slider({
|
|
|
97
97
|
<RadixSlider.Thumb
|
|
98
98
|
style={thumbStyle}
|
|
99
99
|
className={cx(
|
|
100
|
-
"block h-[21px] rounded-sm bg-slider-thumb-background shadow-sliderThumb transition-colors focus:outline-primary focus:outline-2",
|
|
101
|
-
"cursor-ew-resize",
|
|
100
|
+
"n-block n-h-[21px] n-rounded-sm n-bg-slider-thumb-background n-shadow-sliderThumb n-transition-colors focus:n-outline-primary focus:n-outline-2",
|
|
101
|
+
"n-cursor-ew-resize",
|
|
102
102
|
// "outline-none outline outline-2 outline-offset-[0px] outline-background",
|
|
103
|
-
colorScheme === undefined && "border-slider-border",
|
|
104
|
-
colorScheme === "primary" && "border-primary",
|
|
105
|
-
colorScheme === "secondary" && "border-secondary"
|
|
103
|
+
colorScheme === undefined && "n-border-slider-border",
|
|
104
|
+
colorScheme === "primary" && "n-border-primary",
|
|
105
|
+
colorScheme === "secondary" && "n-border-secondary"
|
|
106
106
|
)}
|
|
107
107
|
/>
|
|
108
108
|
</RadixSlider.Root>
|
|
@@ -14,7 +14,7 @@ const SpacerVertical = React.forwardRef<HTMLSpanElement, Props>(
|
|
|
14
14
|
({ size, inline, ...props }, ref: ForwardedRef<HTMLSpanElement>) => {
|
|
15
15
|
return (
|
|
16
16
|
<span
|
|
17
|
-
className={`${inline ? "inline-block" : "block"} ${size === undefined ? "flex flex-1" : ""}`}
|
|
17
|
+
className={`${inline ? "n-inline-block" : "n-block"} ${size === undefined ? "n-flex n-flex-1" : ""}`}
|
|
18
18
|
style={
|
|
19
19
|
size
|
|
20
20
|
? {
|
|
@@ -37,7 +37,7 @@ const SpacerHorizontal = React.forwardRef<HTMLSpanElement, Props>(
|
|
|
37
37
|
({ size, inline, ...props }, ref: ForwardedRef<HTMLSpanElement>) => {
|
|
38
38
|
return (
|
|
39
39
|
<span
|
|
40
|
-
className={`${inline ? "inline-block" : "block"} ${size === undefined ? "flex flex-1" : ""}`}
|
|
40
|
+
className={`${inline ? "n-inline-block" : "n-block"} ${size === undefined ? "n-flex n-flex-1" : ""}`}
|
|
41
41
|
style={
|
|
42
42
|
size
|
|
43
43
|
? {
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Size } from "@noya-app/noya-geometry";
|
|
4
|
+
import { isDeepEqual } from "@noya-app/noya-utils";
|
|
5
|
+
import { useSize } from "@noya-app/react-utils";
|
|
6
|
+
import React, {
|
|
7
|
+
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { cx } from "../utils/classNames";
|
|
14
|
+
import {
|
|
15
|
+
AnimatePresence,
|
|
16
|
+
AnimatePresenceChildState,
|
|
17
|
+
AnimatePresenceProps,
|
|
18
|
+
} from "./AnimatePresence";
|
|
19
|
+
import { BaseToolbar } from "./BaseToolbar";
|
|
20
|
+
import { Button } from "./Button";
|
|
21
|
+
import { Body } from "./Text";
|
|
22
|
+
|
|
23
|
+
type StackRenderContextValue = Record<string, () => React.ReactNode>;
|
|
24
|
+
|
|
25
|
+
const StackRenderContext = React.createContext<StackRenderContextValue>({});
|
|
26
|
+
|
|
27
|
+
export type StackItem = {
|
|
28
|
+
key: string;
|
|
29
|
+
title?: React.ReactNode;
|
|
30
|
+
right?: React.ReactNode;
|
|
31
|
+
renderContent: () => React.ReactNode;
|
|
32
|
+
/** Force hide/show back button for this item */
|
|
33
|
+
showBackButton?: boolean;
|
|
34
|
+
renderHeader?: StackNavigatorRenderHeader;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type StackNavigatorHeaderProps = {
|
|
38
|
+
onBack?: () => void;
|
|
39
|
+
showBackButton: boolean;
|
|
40
|
+
showDivider: boolean;
|
|
41
|
+
right?: React.ReactNode;
|
|
42
|
+
title?: React.ReactNode;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type StackNavigatorRenderHeader =
|
|
46
|
+
| React.ReactNode
|
|
47
|
+
| ((props: StackNavigatorHeaderProps) => React.ReactNode);
|
|
48
|
+
|
|
49
|
+
export type StackNavigatorProps = {
|
|
50
|
+
/** Items ordered from root to top. The last item is shown. */
|
|
51
|
+
items: StackItem[];
|
|
52
|
+
/** Called when the back button is pressed */
|
|
53
|
+
onBack?: () => void;
|
|
54
|
+
className?: string;
|
|
55
|
+
style?: React.CSSProperties;
|
|
56
|
+
contentClassName?: string;
|
|
57
|
+
/** Whether to show the header. @default true */
|
|
58
|
+
showHeader?: boolean;
|
|
59
|
+
showDivider?: boolean;
|
|
60
|
+
cardStyle?: React.CSSProperties;
|
|
61
|
+
cardClassName?: string;
|
|
62
|
+
renderHeader?: StackNavigatorRenderHeader;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function StackNavigator({
|
|
66
|
+
items,
|
|
67
|
+
onBack,
|
|
68
|
+
className,
|
|
69
|
+
style,
|
|
70
|
+
contentClassName,
|
|
71
|
+
cardStyle,
|
|
72
|
+
cardClassName,
|
|
73
|
+
showDivider = true,
|
|
74
|
+
showHeader = true,
|
|
75
|
+
renderHeader,
|
|
76
|
+
}: StackNavigatorProps) {
|
|
77
|
+
const top = items.at(-1);
|
|
78
|
+
|
|
79
|
+
const previousLengthRef = useRef<number>(items.length);
|
|
80
|
+
const direction = useMemo<"forward" | "back">(() => {
|
|
81
|
+
const previous = previousLengthRef.current;
|
|
82
|
+
previousLengthRef.current = items.length;
|
|
83
|
+
return items.length >= previous ? "forward" : "back";
|
|
84
|
+
}, [items.length]);
|
|
85
|
+
|
|
86
|
+
const [cardSizes, setCardSizes] = useState<Record<string, Size>>({});
|
|
87
|
+
const [containerSize, setContainerSize] = useState<Size | undefined>();
|
|
88
|
+
|
|
89
|
+
const handleSizeChange = useCallback((key: string, size: Size) => {
|
|
90
|
+
setCardSizes((prev) => {
|
|
91
|
+
if (isDeepEqual(prev[key], size)) {
|
|
92
|
+
return prev;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { ...prev, [key]: size };
|
|
96
|
+
});
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!top) {
|
|
101
|
+
setContainerSize(undefined);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const measuredSize = cardSizes[top.key];
|
|
106
|
+
|
|
107
|
+
if (!measuredSize) return;
|
|
108
|
+
|
|
109
|
+
setContainerSize((prev) => {
|
|
110
|
+
if (prev && isDeepEqual(prev, measuredSize)) {
|
|
111
|
+
return prev;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return measuredSize;
|
|
115
|
+
});
|
|
116
|
+
}, [cardSizes, top]);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
setCardSizes((prev) => {
|
|
120
|
+
const next: Record<string, Size> = {};
|
|
121
|
+
let changed = false;
|
|
122
|
+
|
|
123
|
+
for (const item of items) {
|
|
124
|
+
const size = prev[item.key];
|
|
125
|
+
if (size) {
|
|
126
|
+
next[item.key] = size;
|
|
127
|
+
} else {
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (Object.keys(prev).length !== Object.keys(next).length) {
|
|
133
|
+
changed = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return changed ? next : prev;
|
|
137
|
+
});
|
|
138
|
+
}, [items]);
|
|
139
|
+
|
|
140
|
+
const animationStyles =
|
|
141
|
+
useMemo((): AnimatePresenceProps["animationStyles"] => {
|
|
142
|
+
return {
|
|
143
|
+
entering: {
|
|
144
|
+
opacity: 0,
|
|
145
|
+
transform:
|
|
146
|
+
direction === "forward" ? `translateX(20px)` : `translateX(-20px)`,
|
|
147
|
+
transition: `opacity 200ms ease, transform 200ms ease`,
|
|
148
|
+
},
|
|
149
|
+
entered: {
|
|
150
|
+
opacity: 1,
|
|
151
|
+
transform: "translateX(0px)",
|
|
152
|
+
transition: `opacity 200ms ease, transform 200ms ease`,
|
|
153
|
+
},
|
|
154
|
+
exiting: {
|
|
155
|
+
opacity: 0,
|
|
156
|
+
transform:
|
|
157
|
+
direction === "forward" ? `translateX(-20px)` : `translateX(20px)`,
|
|
158
|
+
transition: `opacity 200ms ease, transform 200ms ease`,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}, [direction]);
|
|
162
|
+
|
|
163
|
+
const shouldAnimateSize = containerSize !== undefined;
|
|
164
|
+
|
|
165
|
+
const contentStyle = useMemo(() => {
|
|
166
|
+
if (!containerSize) return undefined;
|
|
167
|
+
|
|
168
|
+
const style: React.CSSProperties = {
|
|
169
|
+
// width: containerSize.width,
|
|
170
|
+
height: containerSize.height,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (shouldAnimateSize) {
|
|
174
|
+
style.transition = `height 200ms ease, width 200ms ease`;
|
|
175
|
+
} else {
|
|
176
|
+
style.transition = "none";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return style;
|
|
180
|
+
}, [containerSize, shouldAnimateSize]);
|
|
181
|
+
|
|
182
|
+
const [animationStates, setAnimationStates] = useState<
|
|
183
|
+
Record<string, AnimatePresenceChildState>
|
|
184
|
+
>({});
|
|
185
|
+
|
|
186
|
+
const [renderContentCache, setRenderContentCache] =
|
|
187
|
+
useState<StackRenderContextValue>(() =>
|
|
188
|
+
Object.fromEntries(items.map((item) => [item.key, item.renderContent]))
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
setRenderContentCache((prev) => {
|
|
193
|
+
let changed = false;
|
|
194
|
+
const next: StackRenderContextValue = { ...prev };
|
|
195
|
+
|
|
196
|
+
for (const item of items) {
|
|
197
|
+
const state = animationStates[item.key];
|
|
198
|
+
const isFrozen = state && state !== "entered";
|
|
199
|
+
const existing = next[item.key];
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
existing === undefined ||
|
|
203
|
+
(!isFrozen && existing !== item.renderContent)
|
|
204
|
+
) {
|
|
205
|
+
next[item.key] = item.renderContent;
|
|
206
|
+
if (existing !== item.renderContent) {
|
|
207
|
+
changed = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return changed ? next : prev;
|
|
213
|
+
});
|
|
214
|
+
}, [animationStates, items]);
|
|
215
|
+
|
|
216
|
+
const handleChildStateChange = useCallback(
|
|
217
|
+
(key: string, state: AnimatePresenceChildState) => {
|
|
218
|
+
if (!key) return;
|
|
219
|
+
|
|
220
|
+
setAnimationStates((prev) => {
|
|
221
|
+
if (state === "exited") {
|
|
222
|
+
if (!(key in prev)) return prev;
|
|
223
|
+
const { [key]: _removed, ...rest } = prev;
|
|
224
|
+
return rest;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (prev[key] === state) return prev;
|
|
228
|
+
return { ...prev, [key]: state };
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (state === "exited") {
|
|
232
|
+
setRenderContentCache((prev) => {
|
|
233
|
+
if (!(key in prev)) return prev;
|
|
234
|
+
const next = { ...prev } as StackRenderContextValue;
|
|
235
|
+
delete next[key];
|
|
236
|
+
return next;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
[]
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (!top) return null;
|
|
244
|
+
|
|
245
|
+
const canGoBack = items.length > 1;
|
|
246
|
+
const showBackButton = (top.showBackButton ?? true) && canGoBack;
|
|
247
|
+
|
|
248
|
+
const headerRenderer = top.renderHeader ?? renderHeader;
|
|
249
|
+
|
|
250
|
+
const renderHeaderContent = () => {
|
|
251
|
+
const headerProps: StackNavigatorHeaderProps = {
|
|
252
|
+
onBack,
|
|
253
|
+
showBackButton,
|
|
254
|
+
showDivider,
|
|
255
|
+
right: top.right,
|
|
256
|
+
title: top.title,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (headerRenderer !== undefined) {
|
|
260
|
+
if (typeof headerRenderer === "function") {
|
|
261
|
+
return headerRenderer(headerProps);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return headerRenderer;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return defaultRenderHeader(headerProps);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div
|
|
272
|
+
className={cx("n-flex n-flex-col", className)}
|
|
273
|
+
style={style}
|
|
274
|
+
onClick={(e) => {
|
|
275
|
+
e.stopPropagation();
|
|
276
|
+
// e.preventDefault();
|
|
277
|
+
}}
|
|
278
|
+
onMouseDown={(e) => {
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
// e.preventDefault();
|
|
281
|
+
}}
|
|
282
|
+
onPointerDown={(e) => {
|
|
283
|
+
e.stopPropagation();
|
|
284
|
+
// e.preventDefault();
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
{showHeader && renderHeaderContent()}
|
|
288
|
+
<div
|
|
289
|
+
className={cx("n-relative n-overflow-hidden", contentClassName)}
|
|
290
|
+
style={contentStyle}
|
|
291
|
+
>
|
|
292
|
+
{/* Cache renderContent so we can keep showing the last frame while cards animate */}
|
|
293
|
+
<StackRenderContext.Provider value={renderContentCache}>
|
|
294
|
+
<AnimatePresence
|
|
295
|
+
duration={200}
|
|
296
|
+
animationStyles={animationStyles}
|
|
297
|
+
skipInitialAnimation
|
|
298
|
+
mode="wait"
|
|
299
|
+
onChildStateChange={handleChildStateChange}
|
|
300
|
+
>
|
|
301
|
+
<StackNavigatorCard
|
|
302
|
+
key={top.key}
|
|
303
|
+
item={top}
|
|
304
|
+
onSizeChange={handleSizeChange}
|
|
305
|
+
style={cardStyle}
|
|
306
|
+
className={cardClassName}
|
|
307
|
+
/>
|
|
308
|
+
</AnimatePresence>
|
|
309
|
+
</StackRenderContext.Provider>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type StackNavigatorCardProps = {
|
|
316
|
+
item: StackItem;
|
|
317
|
+
onSizeChange: (key: string, size: Size) => void;
|
|
318
|
+
style?: React.CSSProperties;
|
|
319
|
+
className?: string;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const StackNavigatorCard = ({
|
|
323
|
+
item,
|
|
324
|
+
onSizeChange,
|
|
325
|
+
style,
|
|
326
|
+
className,
|
|
327
|
+
}: StackNavigatorCardProps) => {
|
|
328
|
+
const ref = React.useRef<HTMLDivElement | null>(null);
|
|
329
|
+
const size = useSize(ref);
|
|
330
|
+
|
|
331
|
+
const renderContext = React.useContext(StackRenderContext);
|
|
332
|
+
const renderContent = renderContext?.[item.key] ?? item.renderContent;
|
|
333
|
+
|
|
334
|
+
React.useEffect(() => {
|
|
335
|
+
if (!size || size.width === 0 || size.height === 0) return;
|
|
336
|
+
|
|
337
|
+
onSizeChange(item.key, size);
|
|
338
|
+
}, [item.key, onSizeChange, size]);
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div ref={ref} className={cx("n-flex n-flex-col", className)} style={style}>
|
|
342
|
+
{renderContent?.()}
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
function defaultRenderHeader({
|
|
348
|
+
showDivider,
|
|
349
|
+
showBackButton,
|
|
350
|
+
onBack,
|
|
351
|
+
right,
|
|
352
|
+
title,
|
|
353
|
+
}: StackNavigatorHeaderProps) {
|
|
354
|
+
return (
|
|
355
|
+
<BaseToolbar
|
|
356
|
+
innerClassName="n-px-1"
|
|
357
|
+
showDivider={showDivider}
|
|
358
|
+
enableAbsoluteBreakpoint={false}
|
|
359
|
+
left={
|
|
360
|
+
showBackButton && (
|
|
361
|
+
<Button
|
|
362
|
+
className="-n-mx-2"
|
|
363
|
+
icon="ArrowLeftIcon"
|
|
364
|
+
onClick={onBack}
|
|
365
|
+
aria-label="Back"
|
|
366
|
+
/>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
right={right}
|
|
370
|
+
>
|
|
371
|
+
{typeof title === "string" || typeof title === "number" ? (
|
|
372
|
+
<Body>{title}</Body>
|
|
373
|
+
) : (
|
|
374
|
+
title
|
|
375
|
+
)}
|
|
376
|
+
</BaseToolbar>
|
|
377
|
+
);
|
|
378
|
+
}
|