@noya-app/noya-designsystem 0.1.80 → 0.1.82
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 +11 -11
- package/CHANGELOG.md +13 -0
- package/dist/chunk-L6E2LZOL.mjs +44 -0
- package/dist/chunk-L6E2LZOL.mjs.map +1 -0
- package/dist/chunk-QDV7OQMJ.mjs +37 -0
- package/dist/chunk-QDV7OQMJ.mjs.map +1 -0
- package/dist/emojis.mjs +2 -1
- package/dist/emojis.mjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.mts +7 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.js +374 -305
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +208 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -4
- package/src/components/Drawer.tsx +8 -5
- package/src/components/EmojiPicker.tsx +13 -1
- package/src/components/Popover.tsx +8 -6
- package/src/components/SegmentedControl.tsx +17 -1
- package/src/components/SelectionToolbar.tsx +88 -71
- package/src/utils/mergeProps.ts +38 -0
- package/tsup.config.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.82",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"@base-ui/react": "1.0.0",
|
|
29
29
|
"@dnd-kit/core": "6.3.1",
|
|
30
30
|
"@dnd-kit/sortable": "10.0.0",
|
|
31
|
-
"@
|
|
31
|
+
"@floating-ui/react": "^0.27.0",
|
|
32
|
+
"@noya-app/noya-colorpicker": "0.1.34",
|
|
32
33
|
"@noya-app/noya-utils": "0.1.9",
|
|
33
34
|
"@noya-app/noya-geometry": "0.1.17",
|
|
34
35
|
"@noya-app/noya-icons": "0.1.18",
|
|
@@ -37,8 +38,6 @@
|
|
|
37
38
|
"@radix-ui/react-compose-refs": "^1.0.0",
|
|
38
39
|
"@radix-ui/primitive": "^1.0.1",
|
|
39
40
|
"@radix-ui/react-utils": "^0.0.5",
|
|
40
|
-
"@radix-ui/react-popper": "1.2.3",
|
|
41
|
-
"@radix-ui/utils": "0.0.3",
|
|
42
41
|
"gemoji": "8.1.0",
|
|
43
42
|
"immer": "9.0.5",
|
|
44
43
|
"kiwi.js": "^1.1.3",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Dialog } from "@base-ui/react/dialog";
|
|
4
|
-
import { useControlledOrUncontrolled } from "@noya-app/react-utils";
|
|
4
|
+
import { composeRefs, useControlledOrUncontrolled } from "@noya-app/react-utils";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
import { useImperativeHandle } from "react";
|
|
7
7
|
import { useTriggerToggle } from "../hooks/useTriggerToggle";
|
|
@@ -123,10 +123,13 @@ export const Drawer = React.memo(
|
|
|
123
123
|
childProps
|
|
124
124
|
);
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
mergedProps.ref = (
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
// Compose anchor tracking ref with existing refs
|
|
127
|
+
mergedProps.ref = composeRefs(
|
|
128
|
+
mergedProps.ref as React.Ref<Element> | undefined,
|
|
129
|
+
(node: Element | null) => {
|
|
130
|
+
if (node) setAnchorElement(node);
|
|
131
|
+
}
|
|
132
|
+
);
|
|
130
133
|
|
|
131
134
|
return React.cloneElement(
|
|
132
135
|
element as React.ReactElement<any>,
|
|
@@ -90,6 +90,7 @@ const SECTION_HEADER_HEIGHT = 24;
|
|
|
90
90
|
const HEADER_HEIGHT = 80; // Search + category tabs
|
|
91
91
|
const FOOTER_HEIGHT = 40;
|
|
92
92
|
const HORIZONTAL_PADDING = 8; // n-px-2 = 8px each side
|
|
93
|
+
const SEGMENTED_CONTROL_ITEM_WIDTH = 31;
|
|
93
94
|
|
|
94
95
|
// Category icons mapping
|
|
95
96
|
const categoryIcons: Record<
|
|
@@ -531,6 +532,17 @@ export const EmojiPicker = memo(function EmojiPicker({
|
|
|
531
532
|
[frequentlyUsed.length]
|
|
532
533
|
);
|
|
533
534
|
|
|
535
|
+
// Calculate ideal gap for segmented control to fit container
|
|
536
|
+
// Subtract n-px-2 padding (8px each side = 16px total) from available width
|
|
537
|
+
const segmentedControlGap = useMemo(() => {
|
|
538
|
+
const numItems = categoryItems.length;
|
|
539
|
+
const availableWidth = size.width - HORIZONTAL_PADDING * 2;
|
|
540
|
+
const totalItemsWidth = numItems * SEGMENTED_CONTROL_ITEM_WIDTH;
|
|
541
|
+
const remainingSpace = availableWidth - totalItemsWidth;
|
|
542
|
+
const numGaps = numItems - 1;
|
|
543
|
+
return numGaps > 0 ? Math.max(0, remainingSpace / numGaps) : 0;
|
|
544
|
+
}, [categoryItems.length, size.width]);
|
|
545
|
+
|
|
534
546
|
// Show hovered emoji, or selected emoji if nothing hovered
|
|
535
547
|
const displayEmoji = hoveredEmoji ?? selectedEmoji;
|
|
536
548
|
const footerText = displayEmoji
|
|
@@ -567,7 +579,7 @@ export const EmojiPicker = memo(function EmojiPicker({
|
|
|
567
579
|
onValueChange={handleCategoryChange}
|
|
568
580
|
allowEmpty
|
|
569
581
|
className="n-flex-nowrap n-px-2"
|
|
570
|
-
|
|
582
|
+
style={{ gap: segmentedControlGap }}
|
|
571
583
|
/>
|
|
572
584
|
<Divider />
|
|
573
585
|
{/* Header: Search + Category Tabs */}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
|
|
4
|
-
import { useControlledOrUncontrolled } from "@noya-app/react-utils";
|
|
4
|
+
import { composeRefs, useControlledOrUncontrolled } from "@noya-app/react-utils";
|
|
5
5
|
import React, { useCallback, useEffect, useId } from "react";
|
|
6
6
|
import { useOpenPortalsControls } from "../contexts/OpenPortalsContext";
|
|
7
7
|
import {
|
|
@@ -99,11 +99,13 @@ export function Popover({
|
|
|
99
99
|
childProps
|
|
100
100
|
);
|
|
101
101
|
|
|
102
|
-
//
|
|
103
|
-
mergedProps.ref = (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
// Compose anchor tracking ref with existing refs
|
|
103
|
+
mergedProps.ref = composeRefs(
|
|
104
|
+
mergedProps.ref as React.Ref<Element> | undefined,
|
|
105
|
+
(node: Element | null) => {
|
|
106
|
+
if (node) setAnchorElement(node);
|
|
107
|
+
}
|
|
108
|
+
);
|
|
107
109
|
|
|
108
110
|
return React.cloneElement(
|
|
109
111
|
element as React.ReactElement<any>,
|
|
@@ -209,6 +209,9 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
209
209
|
width: number;
|
|
210
210
|
} | null>(null);
|
|
211
211
|
|
|
212
|
+
// Trigger recalculation when container resizes
|
|
213
|
+
const [resizeTrigger, setResizeTrigger] = useState(0);
|
|
214
|
+
|
|
212
215
|
const selectedIndex = useMemo(() => {
|
|
213
216
|
if (!value) return -1;
|
|
214
217
|
return items.findIndex((item) => item.value === value);
|
|
@@ -238,7 +241,20 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
238
241
|
|
|
239
242
|
// Enable animation after first measurement
|
|
240
243
|
hasInitialized.current = true;
|
|
241
|
-
}, [selectedIndex, variant, items.length]);
|
|
244
|
+
}, [selectedIndex, variant, items.length, resizeTrigger]);
|
|
245
|
+
|
|
246
|
+
// Watch for container resize to recalculate indicator position
|
|
247
|
+
useLayoutEffect(() => {
|
|
248
|
+
const container = containerRef.current;
|
|
249
|
+
if (!container || variant !== "default") return;
|
|
250
|
+
|
|
251
|
+
const observer = new ResizeObserver(() => {
|
|
252
|
+
setResizeTrigger((prev) => prev + 1);
|
|
253
|
+
});
|
|
254
|
+
observer.observe(container);
|
|
255
|
+
|
|
256
|
+
return () => observer.disconnect();
|
|
257
|
+
}, [variant]);
|
|
242
258
|
|
|
243
259
|
const handleSelect = useCallback(
|
|
244
260
|
(newValue: string) => {
|
|
@@ -8,47 +8,44 @@ import {
|
|
|
8
8
|
} from "@noya-app/noya-designsystem";
|
|
9
9
|
import { Rect } from "@noya-app/noya-geometry";
|
|
10
10
|
import { useSize } from "@noya-app/react-utils";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
x: rect.x,
|
|
22
|
-
y: rect.y,
|
|
23
|
-
top: rect.y,
|
|
24
|
-
right: rect.x + rect.width,
|
|
25
|
-
bottom: rect.y + rect.height,
|
|
26
|
-
left: rect.x,
|
|
27
|
-
toJSON: () => null,
|
|
28
|
-
}),
|
|
29
|
-
});
|
|
11
|
+
import {
|
|
12
|
+
useFloating,
|
|
13
|
+
offset,
|
|
14
|
+
flip,
|
|
15
|
+
shift,
|
|
16
|
+
FloatingPortal,
|
|
17
|
+
type Placement,
|
|
18
|
+
type VirtualElement,
|
|
19
|
+
} from "@floating-ui/react";
|
|
20
|
+
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
30
21
|
|
|
31
|
-
type
|
|
32
|
-
|
|
33
|
-
>;
|
|
22
|
+
type Side = "top" | "right" | "bottom" | "left";
|
|
23
|
+
type Align = "start" | "center" | "end";
|
|
34
24
|
|
|
35
|
-
type SelectionToolbarContainerProps =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
type SelectionToolbarContainerProps = {
|
|
26
|
+
side?: Side;
|
|
27
|
+
sideOffset?: number;
|
|
28
|
+
align?: Align;
|
|
29
|
+
avoidCollisions?: boolean;
|
|
39
30
|
rect: Rect;
|
|
40
31
|
children: React.ReactNode;
|
|
41
|
-
portalContainer?: HTMLElement | null;
|
|
42
32
|
containerClassName?: string;
|
|
43
33
|
containerStyle?: React.CSSProperties;
|
|
44
34
|
transitionDuration?: number;
|
|
45
35
|
};
|
|
46
36
|
|
|
37
|
+
// Map side + align to floating-ui placement
|
|
38
|
+
function getPlacement(side: Side, align: Align): Placement {
|
|
39
|
+
if (align === "center") {
|
|
40
|
+
return side;
|
|
41
|
+
}
|
|
42
|
+
return `${side}-${align}` as Placement;
|
|
43
|
+
}
|
|
44
|
+
|
|
47
45
|
export const SelectionToolbarContainer = memo(
|
|
48
46
|
function SelectionToolbarContainer({
|
|
49
47
|
rect,
|
|
50
48
|
children,
|
|
51
|
-
portalContainer,
|
|
52
49
|
side = "top",
|
|
53
50
|
sideOffset = 10,
|
|
54
51
|
align = "center",
|
|
@@ -58,58 +55,78 @@ export const SelectionToolbarContainer = memo(
|
|
|
58
55
|
transitionDuration = 0.2,
|
|
59
56
|
}: SelectionToolbarContainerProps) {
|
|
60
57
|
const portalScopeId = usePortalScopeId();
|
|
61
|
-
const
|
|
62
|
-
|
|
58
|
+
const [containerElement, setContainerElement] =
|
|
59
|
+
useState<HTMLDivElement | null>(null);
|
|
60
|
+
const containerRef = useCallback((el: HTMLDivElement | null) => {
|
|
61
|
+
setContainerElement(el);
|
|
62
|
+
}, []);
|
|
63
|
+
const containerRefObject = useMemo(
|
|
64
|
+
() => ({ current: containerElement }),
|
|
65
|
+
[containerElement]
|
|
66
|
+
);
|
|
67
|
+
const size = useSize(containerRefObject, "width");
|
|
63
68
|
|
|
64
69
|
// Create a virtual reference element from the rect
|
|
65
|
-
const
|
|
66
|
-
() => ({
|
|
67
|
-
|
|
70
|
+
const virtualElement = useMemo(
|
|
71
|
+
(): VirtualElement => ({
|
|
72
|
+
getBoundingClientRect: () => ({
|
|
73
|
+
width: rect.width,
|
|
74
|
+
height: rect.height,
|
|
75
|
+
x: rect.x,
|
|
76
|
+
y: rect.y,
|
|
77
|
+
top: rect.y,
|
|
78
|
+
right: rect.x + rect.width,
|
|
79
|
+
bottom: rect.y + rect.height,
|
|
80
|
+
left: rect.x,
|
|
81
|
+
}),
|
|
68
82
|
}),
|
|
69
83
|
[rect]
|
|
70
84
|
);
|
|
71
85
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}}
|
|
88
|
-
>
|
|
86
|
+
const placement = getPlacement(side, align);
|
|
87
|
+
|
|
88
|
+
const { refs, floatingStyles } = useFloating({
|
|
89
|
+
placement,
|
|
90
|
+
middleware: avoidCollisions
|
|
91
|
+
? [offset(sideOffset), flip(), shift({ padding: 10 })]
|
|
92
|
+
: [offset(sideOffset)],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
refs.setPositionReference(virtualElement);
|
|
97
|
+
}, [refs, virtualElement]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<FloatingPortal>
|
|
89
101
|
<div
|
|
90
|
-
ref={
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
ref={refs.setFloating}
|
|
103
|
+
{...portalScopeProps(portalScopeId)}
|
|
104
|
+
contentEditable={false}
|
|
105
|
+
style={{
|
|
106
|
+
...floatingStyles,
|
|
107
|
+
opacity: size ? 1 : 0,
|
|
108
|
+
transition: `opacity ${transitionDuration}s ease-in-out`,
|
|
109
|
+
[cssVarNames.colors.inputBackground]: "transparent",
|
|
110
|
+
[cssVarNames.colors.buttonBackground]: "transparent",
|
|
111
|
+
}}
|
|
112
|
+
className="n-z-menu"
|
|
100
113
|
>
|
|
101
|
-
|
|
114
|
+
<div
|
|
115
|
+
ref={containerRef}
|
|
116
|
+
className={cx(
|
|
117
|
+
"n-flex n-gap-1 n-bg-popover-background n-rounded n-shadow-popover n-p-1",
|
|
118
|
+
containerClassName
|
|
119
|
+
)}
|
|
120
|
+
style={containerStyle}
|
|
121
|
+
onClick={(e) => e.stopPropagation()}
|
|
122
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
123
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
124
|
+
onWheel={(e) => e.stopPropagation()}
|
|
125
|
+
>
|
|
126
|
+
{children}
|
|
127
|
+
</div>
|
|
102
128
|
</div>
|
|
103
|
-
</
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<PopperPrimitive.Root>
|
|
108
|
-
<PopperPrimitive.Anchor virtualRef={virtualRef} />
|
|
109
|
-
{portalContainer
|
|
110
|
-
? createPortal(popperContent, portalContainer)
|
|
111
|
-
: popperContent}
|
|
112
|
-
</PopperPrimitive.Root>
|
|
129
|
+
</FloatingPortal>
|
|
113
130
|
);
|
|
114
131
|
}
|
|
115
132
|
);
|
package/src/utils/mergeProps.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assignRef } from "@noya-app/react-utils";
|
|
1
2
|
import React from "react";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -31,6 +32,43 @@ export function mergePropsWithEventComposition(
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// Compose refs so both trigger and child refs get called
|
|
36
|
+
if (triggerProps.ref || childProps.ref) {
|
|
37
|
+
const triggerRef = triggerProps.ref as React.Ref<unknown> | undefined;
|
|
38
|
+
const childRef = childProps.ref as React.Ref<unknown> | undefined;
|
|
39
|
+
if (triggerRef && childRef) {
|
|
40
|
+
mergedProps.ref = (value: unknown) => {
|
|
41
|
+
assignRef(triggerRef as React.ForwardedRef<unknown>, value);
|
|
42
|
+
assignRef(childRef as React.ForwardedRef<unknown>, value);
|
|
43
|
+
};
|
|
44
|
+
} else if (childRef) {
|
|
45
|
+
mergedProps.ref = childRef;
|
|
46
|
+
}
|
|
47
|
+
// If only triggerRef exists, it's already in mergedProps from the spread
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Merge style objects (child styles take precedence)
|
|
51
|
+
if (
|
|
52
|
+
typeof triggerProps.style === "object" &&
|
|
53
|
+
triggerProps.style !== null &&
|
|
54
|
+
typeof childProps.style === "object" &&
|
|
55
|
+
childProps.style !== null
|
|
56
|
+
) {
|
|
57
|
+
mergedProps.style = { ...triggerProps.style, ...childProps.style };
|
|
58
|
+
} else if (childProps.style !== undefined) {
|
|
59
|
+
mergedProps.style = childProps.style;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Merge className strings
|
|
63
|
+
if (triggerProps.className || childProps.className) {
|
|
64
|
+
const classes = [triggerProps.className, childProps.className]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join(" ");
|
|
67
|
+
if (classes) {
|
|
68
|
+
mergedProps.className = classes;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
34
72
|
return mergedProps;
|
|
35
73
|
}
|
|
36
74
|
|
package/tsup.config.ts
CHANGED
|
@@ -12,6 +12,14 @@ export default defineConfig({
|
|
|
12
12
|
sourcemap: true,
|
|
13
13
|
// Bundle base-ui to avoid commonjs "react" import errors in vite projects
|
|
14
14
|
noExternal: ["@base-ui/react", "@base-ui/utils", "use-sync-external-store"],
|
|
15
|
+
// Shim require("react") for CJS code running in ESM/browser context
|
|
16
|
+
esbuildOptions(options, context) {
|
|
17
|
+
if (context.format === "esm") {
|
|
18
|
+
options.banner = {
|
|
19
|
+
js: `import * as __noya_react__ from "react";const require=(m)=>{if(m==="react")return __noya_react__;throw new Error("Dynamic require not supported: "+m)};`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
},
|
|
15
23
|
async onSuccess() {
|
|
16
24
|
// Build Tailwind CSS after successful tsup build
|
|
17
25
|
try {
|