@noya-app/noya-designsystem 0.1.94 → 0.1.95
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 +9 -9
- package/CHANGELOG.md +10 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +158 -93
- package/dist/index.d.ts +158 -93
- package/dist/index.js +1307 -1034
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1231 -962
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/SharedDragProvider.test.ts +72 -29
- package/src/__tests__/fuzzyScorer.test.ts +49 -1
- package/src/components/Button.tsx +1 -1
- package/src/components/Combobox.tsx +23 -3
- package/src/components/ComboboxMenu.tsx +32 -15
- package/src/components/ContextMenu.tsx +26 -7
- package/src/components/DimensionInput.tsx +12 -2
- package/src/components/DropdownMenu.tsx +23 -8
- package/src/components/InputField.tsx +1 -0
- package/src/components/InspectorContainer.tsx +16 -1
- package/src/components/List.tsx +25 -3
- package/src/components/Popover.tsx +13 -1
- package/src/components/Section.tsx +35 -11
- package/src/components/SegmentedControl.tsx +14 -4
- package/src/components/Toolbar.tsx +16 -1
- package/src/components/ToolbarItems.tsx +322 -0
- package/src/components/Tooltip.tsx +8 -1
- package/src/components/__tests__/ToolbarItems.test.tsx +205 -0
- package/src/components/internal/Menu.tsx +4 -1
- package/src/components/internal/TextInput.tsx +43 -2
- package/src/components/internal/__tests__/TextInput.test.tsx +77 -0
- package/src/components/listView/ListViewEditableRowTitle.tsx +37 -1
- package/src/components/listView/ListViewRow.tsx +3 -0
- package/src/components/listView/__tests__/ListViewEditableRowTitle.test.tsx +69 -0
- package/src/components/sorting/Sortable.tsx +4 -1
- package/src/index.tsx +0 -1
- package/src/utils/combobox.ts +22 -5
- package/src/utils/fuzzyScorer.ts +71 -0
- package/tailwind.config.ts +7 -0
- package/src/components/InspectorPrimitives.tsx +0 -188
|
@@ -31,6 +31,8 @@ interface Props {
|
|
|
31
31
|
style?: React.CSSProperties;
|
|
32
32
|
portalContainer?: HTMLElement | null;
|
|
33
33
|
side?: "top" | "bottom" | "left" | "right";
|
|
34
|
+
/** Cross-axis alignment against the anchor. @default "center" */
|
|
35
|
+
align?: "start" | "center" | "end";
|
|
34
36
|
initialFocus?: React.ComponentProps<
|
|
35
37
|
typeof PopoverPrimitive.Popup
|
|
36
38
|
>["initialFocus"];
|
|
@@ -39,6 +41,13 @@ interface Props {
|
|
|
39
41
|
* Return false to prevent the popover from closing.
|
|
40
42
|
*/
|
|
41
43
|
shouldCloseOnInteractOutside?: (event: PointerEvent) => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Whether the trigger ultimately renders a native `<button>`. Pass false
|
|
46
|
+
* for non-button triggers (e.g. an input container) so Base UI doesn't log
|
|
47
|
+
* a nativeButton mismatch console error.
|
|
48
|
+
* @default true
|
|
49
|
+
*/
|
|
50
|
+
nativeButtonTrigger?: boolean;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export const popoverStyle = {
|
|
@@ -54,6 +63,7 @@ export function Popover({
|
|
|
54
63
|
closable,
|
|
55
64
|
open,
|
|
56
65
|
side,
|
|
66
|
+
align = "center",
|
|
57
67
|
sideOffset = 4,
|
|
58
68
|
showArrow = true,
|
|
59
69
|
onOpenChange,
|
|
@@ -63,6 +73,7 @@ export function Popover({
|
|
|
63
73
|
style,
|
|
64
74
|
initialFocus,
|
|
65
75
|
shouldCloseOnInteractOutside,
|
|
76
|
+
nativeButtonTrigger = true,
|
|
66
77
|
}: Props) {
|
|
67
78
|
const defaultId = useId();
|
|
68
79
|
const portalScopeId = usePortalScopeId();
|
|
@@ -91,6 +102,7 @@ export function Popover({
|
|
|
91
102
|
return (
|
|
92
103
|
<PopoverPrimitive.Root open={isOpen} onOpenChange={handleOpenChange}>
|
|
93
104
|
<PopoverPrimitive.Trigger
|
|
105
|
+
nativeButton={nativeButtonTrigger}
|
|
94
106
|
render={(triggerProps) => {
|
|
95
107
|
const element = React.isValidElement(trigger) ? (
|
|
96
108
|
trigger
|
|
@@ -122,7 +134,7 @@ export function Popover({
|
|
|
122
134
|
{...portalScopeProps(portalScopeId)}
|
|
123
135
|
anchor={anchorElement}
|
|
124
136
|
side={side}
|
|
125
|
-
align=
|
|
137
|
+
align={align}
|
|
126
138
|
sideOffset={sideOffset}
|
|
127
139
|
collisionPadding={8}
|
|
128
140
|
arrowPadding={2}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
IconName,
|
|
7
7
|
IndentContext,
|
|
8
8
|
INPUT_HEIGHT,
|
|
9
|
-
InspectorPrimitives,
|
|
10
9
|
renderIcon,
|
|
11
10
|
Spacer,
|
|
12
11
|
Text,
|
|
@@ -14,6 +13,37 @@ import {
|
|
|
14
13
|
} from "@noya-app/noya-designsystem";
|
|
15
14
|
import { clientStorage, usePersistentStateString } from "@noya-app/react-utils";
|
|
16
15
|
import React, { CSSProperties, memo, useMemo } from "react";
|
|
16
|
+
import { cx } from "../utils/classNames";
|
|
17
|
+
|
|
18
|
+
// Local copies of the bare section-header shells (the shared set moved to
|
|
19
|
+
// @noya-app/inspector as `FieldLayout`, which this package cannot depend on
|
|
20
|
+
// without a cycle). Keep the class lists in sync with FieldLayout.
|
|
21
|
+
const SectionHeaderShell = ({
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
25
|
+
<div className={cx(`n-flex n-items-center`, className)} {...props} />
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const TitleShell = ({
|
|
29
|
+
className,
|
|
30
|
+
$textStyle,
|
|
31
|
+
...props
|
|
32
|
+
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
33
|
+
$textStyle?: "small" | "heading5" | "heading4" | "heading3";
|
|
34
|
+
}) => (
|
|
35
|
+
<div
|
|
36
|
+
className={cx(
|
|
37
|
+
`n-flex n-select-none ${
|
|
38
|
+
$textStyle
|
|
39
|
+
? `${textStyles[$textStyle]} n-text-text`
|
|
40
|
+
: "n-font-sans n-text-label n-text-text-muted n-font-semibold"
|
|
41
|
+
} `,
|
|
42
|
+
className
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
17
47
|
|
|
18
48
|
const titleIconStyle: CSSProperties = {
|
|
19
49
|
marginRight: "6px",
|
|
@@ -57,19 +87,13 @@ export function SectionHeader({
|
|
|
57
87
|
);
|
|
58
88
|
|
|
59
89
|
const titleElement = title ? (
|
|
60
|
-
<
|
|
61
|
-
$textStyle={titleTextStyle}
|
|
62
|
-
className={titleClassName}
|
|
63
|
-
>
|
|
90
|
+
<TitleShell $textStyle={titleTextStyle} className={titleClassName}>
|
|
64
91
|
{title}
|
|
65
|
-
</
|
|
92
|
+
</TitleShell>
|
|
66
93
|
) : null;
|
|
67
94
|
|
|
68
95
|
return (
|
|
69
|
-
<
|
|
70
|
-
style={headerStyle}
|
|
71
|
-
className={className}
|
|
72
|
-
>
|
|
96
|
+
<SectionHeaderShell style={headerStyle} className={className}>
|
|
73
97
|
{titleIcon && (
|
|
74
98
|
<div style={titleIconStyle} className={textStyles[titleTextStyle]}>
|
|
75
99
|
{"\u200B"}
|
|
@@ -98,7 +122,7 @@ export function SectionHeader({
|
|
|
98
122
|
</Text>
|
|
99
123
|
)}
|
|
100
124
|
{right}
|
|
101
|
-
</
|
|
125
|
+
</SectionHeaderShell>
|
|
102
126
|
);
|
|
103
127
|
}
|
|
104
128
|
|
|
@@ -30,7 +30,11 @@ const EXPANSION_THRESHOLD_PX = 20;
|
|
|
30
30
|
// Debounce delay (ms) before re-enabling indicator transition after resize stops
|
|
31
31
|
const RESIZE_SETTLE_MS = 150;
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* "primaryTint" is Figma-UI3-style: a light primary-tinted active segment
|
|
35
|
+
* with a primary-colored glyph, instead of the solid primary fill.
|
|
36
|
+
*/
|
|
37
|
+
type SegmentedControlColorScheme = "primary" | "secondary" | "primaryTint";
|
|
34
38
|
type SegmentedControlVariant = "default" | "tabs" | "buttons";
|
|
35
39
|
|
|
36
40
|
type SegmentedControlContextValue = {
|
|
@@ -187,12 +191,16 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
|
187
191
|
? // default variant: only text color, background handled by sliding indicator
|
|
188
192
|
colorScheme === "secondary" || colorScheme === "primary"
|
|
189
193
|
? "data-[pressed]:n-text-white"
|
|
190
|
-
: "
|
|
194
|
+
: colorScheme === "primaryTint"
|
|
195
|
+
? "data-[pressed]:n-text-primary"
|
|
196
|
+
: "data-[pressed]:n-text-segmented-control-item-active-text"
|
|
191
197
|
: colorScheme === "secondary"
|
|
192
198
|
? "data-[pressed]:n-bg-secondary data-[pressed]:n-text-white"
|
|
193
199
|
: colorScheme === "primary"
|
|
194
200
|
? "data-[pressed]:n-bg-primary data-[pressed]:n-text-white"
|
|
195
|
-
:
|
|
201
|
+
: colorScheme === "primaryTint"
|
|
202
|
+
? "data-[pressed]:n-bg-primary-pastel data-[pressed]:n-text-primary"
|
|
203
|
+
: "data-[pressed]:n-bg-segmented-control-item-active-background data-[pressed]:n-text-segmented-control-item-active-text",
|
|
196
204
|
"focus-visible:n-z-interactable",
|
|
197
205
|
// checked default (all colorSchemes) - hide separators near selected
|
|
198
206
|
variant === "default" &&
|
|
@@ -485,7 +493,9 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
485
493
|
? "n-bg-secondary"
|
|
486
494
|
: colorScheme === "primary"
|
|
487
495
|
? "n-bg-primary"
|
|
488
|
-
: "
|
|
496
|
+
: colorScheme === "primaryTint"
|
|
497
|
+
? "n-bg-primary-pastel"
|
|
498
|
+
: "n-bg-segmented-control-item-active-background";
|
|
489
499
|
|
|
490
500
|
return (
|
|
491
501
|
<SegmentedControlContext.Provider value={contextValue}>
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
import React, { forwardRef, HTMLAttributes } from "react";
|
|
4
4
|
import { cx } from "../utils/classNames";
|
|
5
5
|
import { toolbarTransparentStyle } from "./BaseToolbar";
|
|
6
|
+
import { ToolbarItems } from "./ToolbarItems";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
ToolbarGroupItem,
|
|
10
|
+
ToolbarItem,
|
|
11
|
+
ToolbarItemsProps,
|
|
12
|
+
ToolbarPopoverItem,
|
|
13
|
+
ToolbarSeparatorItem,
|
|
14
|
+
} from "./ToolbarItems";
|
|
15
|
+
export { ToolbarItems } from "./ToolbarItems";
|
|
6
16
|
|
|
7
17
|
export type ToolbarSize = "small" | "medium";
|
|
8
18
|
|
|
@@ -14,7 +24,7 @@ export interface ToolbarProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
14
24
|
size?: ToolbarSize;
|
|
15
25
|
}
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
const ToolbarRoot = forwardRef<HTMLDivElement, ToolbarProps>(
|
|
18
28
|
function Toolbar(
|
|
19
29
|
{ children, className, role = "toolbar", size = "medium", style, ...props },
|
|
20
30
|
ref
|
|
@@ -36,3 +46,8 @@ export const Toolbar = forwardRef<HTMLDivElement, ToolbarProps>(
|
|
|
36
46
|
);
|
|
37
47
|
}
|
|
38
48
|
);
|
|
49
|
+
|
|
50
|
+
/** The floating toolbar surface. `Toolbar.Items` renders a declarative
|
|
51
|
+
* `ToolbarItem[]` (icon buttons, separators, split-button groups, popovers)
|
|
52
|
+
* inside it. */
|
|
53
|
+
export const Toolbar = Object.assign(ToolbarRoot, { Items: ToolbarItems });
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { ReactNode, useEffect, useState } from "react";
|
|
4
|
+
import { cx } from "../utils/classNames";
|
|
5
|
+
import { ToolbarShortcut } from "./AppToolbar";
|
|
6
|
+
import { DividerVertical } from "./Divider";
|
|
7
|
+
import { DropdownMenu } from "./DropdownMenu";
|
|
8
|
+
import { renderIcon } from "./Icons";
|
|
9
|
+
import { MenuItemIcon, SelectableMenuItem } from "./internal/Menu";
|
|
10
|
+
import { Popover } from "./Popover";
|
|
11
|
+
import { Tooltip } from "./Tooltip";
|
|
12
|
+
|
|
13
|
+
export type ToolbarSeparatorItem = { type: "separator" };
|
|
14
|
+
|
|
15
|
+
/** A split-button slot: the primary button activates the group's
|
|
16
|
+
* representative tool and the adjacent chevron opens a flyout menu listing
|
|
17
|
+
* every item in the group. */
|
|
18
|
+
export type ToolbarGroupItem<T extends string> = {
|
|
19
|
+
type: "group";
|
|
20
|
+
id: string;
|
|
21
|
+
items: SelectableMenuItem<T>[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** A button (icon or text) that opens a Popover with arbitrary content. */
|
|
25
|
+
export type ToolbarPopoverItem = {
|
|
26
|
+
type: "popover";
|
|
27
|
+
id: string;
|
|
28
|
+
title: ReactNode;
|
|
29
|
+
icon?: MenuItemIcon;
|
|
30
|
+
content: ReactNode | ((close: () => void) => ReactNode);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A declarative item for `ToolbarItems` (`Toolbar.Items`). Selectable items
|
|
35
|
+
* reuse the menu system's `SelectableMenuItem`, so the same definitions can
|
|
36
|
+
* feed a toolbar, a dropdown, or a context menu.
|
|
37
|
+
*/
|
|
38
|
+
export type ToolbarItem<T extends string> =
|
|
39
|
+
| SelectableMenuItem<T>
|
|
40
|
+
| ToolbarSeparatorItem
|
|
41
|
+
| ToolbarGroupItem<T>
|
|
42
|
+
| ToolbarPopoverItem;
|
|
43
|
+
|
|
44
|
+
type PopupSide = "top" | "bottom" | "left" | "right";
|
|
45
|
+
|
|
46
|
+
export interface ToolbarItemsProps<T extends string> {
|
|
47
|
+
items: ToolbarItem<T>[];
|
|
48
|
+
onSelect: (value: T) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Side the group flyouts and popovers open on. Bottom-of-screen toolbars
|
|
51
|
+
* want "top".
|
|
52
|
+
* @default "top"
|
|
53
|
+
*/
|
|
54
|
+
side?: PopupSide;
|
|
55
|
+
/** @default "top" */
|
|
56
|
+
tooltipSide?: PopupSide;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const POPUP_OFFSET = 10;
|
|
60
|
+
|
|
61
|
+
function labelForTitle(title: ReactNode): string | undefined {
|
|
62
|
+
return typeof title === "string" ? title : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Square icon button styled by the standard light tokens: transparent on the
|
|
66
|
+
* toolbar surface, `input-background-light` on hover, and the solid primary
|
|
67
|
+
* square with a white glyph when checked (the design system's active-tool
|
|
68
|
+
* treatment). */
|
|
69
|
+
function ToolbarItemButton<T extends string>({
|
|
70
|
+
item,
|
|
71
|
+
tooltipSide,
|
|
72
|
+
onClick,
|
|
73
|
+
}: {
|
|
74
|
+
item: SelectableMenuItem<T>;
|
|
75
|
+
tooltipSide: PopupSide;
|
|
76
|
+
onClick: () => void;
|
|
77
|
+
}) {
|
|
78
|
+
const label = labelForTitle(item.title);
|
|
79
|
+
|
|
80
|
+
const button = (
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
aria-label={label}
|
|
84
|
+
aria-pressed={item.checked ?? false}
|
|
85
|
+
disabled={item.disabled}
|
|
86
|
+
onClick={onClick}
|
|
87
|
+
className={cx(
|
|
88
|
+
"n-flex n-h-input-height n-flex-none n-cursor-default n-items-center n-justify-center n-rounded n-border-0 n-outline-none n-transition-colors focus-visible:n-ring-2 focus-visible:n-ring-primary",
|
|
89
|
+
// Icon items are square at the design system's floating-toolbar
|
|
90
|
+
// control size (27px, see Button variant="floating" and
|
|
91
|
+
// INPUT_HEIGHT); text titles get auto width with the same padding
|
|
92
|
+
// and type style as the popover text trigger.
|
|
93
|
+
item.icon
|
|
94
|
+
? "n-w-input-height n-p-0"
|
|
95
|
+
: "n-px-2 n-py-0 n-font-sans n-text-[12px] n-font-medium",
|
|
96
|
+
item.checked
|
|
97
|
+
? "n-bg-primary n-text-white"
|
|
98
|
+
: "n-bg-transparent n-text-text hover:n-bg-input-background-light",
|
|
99
|
+
item.disabled && "n-pointer-events-none n-opacity-30"
|
|
100
|
+
)}
|
|
101
|
+
>
|
|
102
|
+
{item.icon ? renderIcon(item.icon) : item.title}
|
|
103
|
+
</button>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (item.disabled) return button;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Tooltip
|
|
110
|
+
side={tooltipSide}
|
|
111
|
+
sideOffset={POPUP_OFFSET}
|
|
112
|
+
content={
|
|
113
|
+
item.tooltip ??
|
|
114
|
+
(item.shortcut ? (
|
|
115
|
+
<ToolbarShortcut shortcut={item.shortcut} label={item.title} />
|
|
116
|
+
) : (
|
|
117
|
+
item.title
|
|
118
|
+
))
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
{button}
|
|
122
|
+
</Tooltip>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Returns a plain <button> element (not a component) so DropdownMenu's
|
|
127
|
+
* native-button trigger detection works. */
|
|
128
|
+
function renderGroupChevron({
|
|
129
|
+
label,
|
|
130
|
+
open,
|
|
131
|
+
disabled,
|
|
132
|
+
}: {
|
|
133
|
+
label: string;
|
|
134
|
+
open: boolean;
|
|
135
|
+
disabled?: boolean;
|
|
136
|
+
}) {
|
|
137
|
+
return (
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
aria-label={label}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
className={cx(
|
|
143
|
+
"n--ml-0.5 n-flex n-h-input-height n-w-3.5 n-flex-none n-cursor-default n-items-center n-justify-center n-rounded n-border-0 n-p-0 n-outline-none n-transition-colors focus-visible:n-ring-2 focus-visible:n-ring-primary",
|
|
144
|
+
open
|
|
145
|
+
? "n-bg-input-background-light n-text-text"
|
|
146
|
+
: "n-bg-transparent n-text-text-muted hover:n-bg-input-background-light hover:n-text-text",
|
|
147
|
+
disabled && "n-pointer-events-none n-opacity-30"
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
{renderIcon("DropdownChevronIcon")}
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The split-button group: primary button shows the representative item
|
|
156
|
+
* (checked child > last-used child > first child) and the narrow chevron
|
|
157
|
+
* opens the group's items in a dropdown flyout. */
|
|
158
|
+
function ToolbarItemGroup<T extends string>({
|
|
159
|
+
group,
|
|
160
|
+
onSelect,
|
|
161
|
+
side,
|
|
162
|
+
tooltipSide,
|
|
163
|
+
}: {
|
|
164
|
+
group: ToolbarGroupItem<T>;
|
|
165
|
+
onSelect: (value: T) => void;
|
|
166
|
+
side: PopupSide;
|
|
167
|
+
tooltipSide: PopupSide;
|
|
168
|
+
}) {
|
|
169
|
+
const { items } = group;
|
|
170
|
+
const checkedItem = items.find((item) => item.checked);
|
|
171
|
+
const [lastUsed, setLastUsed] = useState<T | undefined>(undefined);
|
|
172
|
+
|
|
173
|
+
const checkedValue = checkedItem?.value;
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
if (checkedValue !== undefined) setLastUsed(checkedValue);
|
|
176
|
+
}, [checkedValue]);
|
|
177
|
+
|
|
178
|
+
const representative =
|
|
179
|
+
checkedItem ?? items.find((item) => item.value === lastUsed) ?? items[0];
|
|
180
|
+
|
|
181
|
+
if (!representative) return null;
|
|
182
|
+
|
|
183
|
+
const allDisabled = items.every((item) => item.disabled);
|
|
184
|
+
const label = labelForTitle(representative.title);
|
|
185
|
+
const chevronLabel = `${label ?? "More"} tools`;
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div className="n-flex n-items-center">
|
|
189
|
+
<ToolbarItemButton
|
|
190
|
+
item={{ ...representative, checked: checkedItem !== undefined }}
|
|
191
|
+
tooltipSide={tooltipSide}
|
|
192
|
+
onClick={() => {
|
|
193
|
+
setLastUsed(representative.value);
|
|
194
|
+
onSelect(representative.value);
|
|
195
|
+
}}
|
|
196
|
+
/>
|
|
197
|
+
{allDisabled ? (
|
|
198
|
+
// Not wrapped in DropdownMenu (whose trigger would re-enable the
|
|
199
|
+
// native button) — a fully disabled group has nothing to open.
|
|
200
|
+
renderGroupChevron({
|
|
201
|
+
label: chevronLabel,
|
|
202
|
+
open: false,
|
|
203
|
+
disabled: true,
|
|
204
|
+
})
|
|
205
|
+
) : (
|
|
206
|
+
<DropdownMenu
|
|
207
|
+
items={items}
|
|
208
|
+
side={side}
|
|
209
|
+
sideOffset={POPUP_OFFSET + 4}
|
|
210
|
+
// Pin the flyout's left edge to the group instead of letting it
|
|
211
|
+
// float centered under the narrow chevron (27px button + 2px gap).
|
|
212
|
+
align="start"
|
|
213
|
+
alignOffset={-29}
|
|
214
|
+
shouldBindKeyboardShortcuts={false}
|
|
215
|
+
onSelect={(value) => {
|
|
216
|
+
setLastUsed(value);
|
|
217
|
+
onSelect(value);
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
{({ open }) =>
|
|
221
|
+
renderGroupChevron({
|
|
222
|
+
label: chevronLabel,
|
|
223
|
+
open,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
</DropdownMenu>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function ToolbarItemPopover({
|
|
233
|
+
item,
|
|
234
|
+
side,
|
|
235
|
+
}: {
|
|
236
|
+
item: ToolbarPopoverItem;
|
|
237
|
+
side: PopupSide;
|
|
238
|
+
}) {
|
|
239
|
+
const [open, setOpen] = useState(false);
|
|
240
|
+
const label = labelForTitle(item.title);
|
|
241
|
+
|
|
242
|
+
const trigger = (
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
aria-label={label}
|
|
246
|
+
className={cx(
|
|
247
|
+
"n-flex n-h-input-height n-flex-none n-cursor-default n-items-center n-justify-center n-rounded n-border-0 n-outline-none n-transition-colors focus-visible:n-ring-2 focus-visible:n-ring-primary",
|
|
248
|
+
item.icon
|
|
249
|
+
? "n-w-input-height n-p-0"
|
|
250
|
+
: "n-px-2 n-font-sans n-text-[12px] n-font-medium",
|
|
251
|
+
open
|
|
252
|
+
? "n-bg-input-background-light n-text-text"
|
|
253
|
+
: "n-bg-transparent n-text-text hover:n-bg-input-background-light"
|
|
254
|
+
)}
|
|
255
|
+
>
|
|
256
|
+
{item.icon ? renderIcon(item.icon) : item.title}
|
|
257
|
+
</button>
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
return (
|
|
261
|
+
<Popover
|
|
262
|
+
open={open}
|
|
263
|
+
onOpenChange={setOpen}
|
|
264
|
+
side={side}
|
|
265
|
+
showArrow={false}
|
|
266
|
+
sideOffset={POPUP_OFFSET}
|
|
267
|
+
trigger={trigger}
|
|
268
|
+
>
|
|
269
|
+
{typeof item.content === "function"
|
|
270
|
+
? item.content(() => setOpen(false))
|
|
271
|
+
: item.content}
|
|
272
|
+
</Popover>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Renders a flat list of `ToolbarItem`s inside a `Toolbar`, styled entirely
|
|
278
|
+
* by the standard (light) theme tokens. Also available as `Toolbar.Items`.
|
|
279
|
+
*/
|
|
280
|
+
export function ToolbarItems<T extends string>({
|
|
281
|
+
items,
|
|
282
|
+
onSelect,
|
|
283
|
+
side = "top",
|
|
284
|
+
tooltipSide = "top",
|
|
285
|
+
}: ToolbarItemsProps<T>) {
|
|
286
|
+
return (
|
|
287
|
+
<>
|
|
288
|
+
{items.map((item, index) => {
|
|
289
|
+
if (item.type === "separator") {
|
|
290
|
+
return <DividerVertical key={`separator-${index}`} />;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (item.type === "group") {
|
|
294
|
+
return (
|
|
295
|
+
<ToolbarItemGroup
|
|
296
|
+
key={item.id}
|
|
297
|
+
group={item}
|
|
298
|
+
onSelect={onSelect}
|
|
299
|
+
side={side}
|
|
300
|
+
tooltipSide={tooltipSide}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (item.type === "popover") {
|
|
306
|
+
return (
|
|
307
|
+
<ToolbarItemPopover key={item.id} item={item} side={side} />
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<ToolbarItemButton
|
|
313
|
+
key={item.value}
|
|
314
|
+
item={item}
|
|
315
|
+
tooltipSide={tooltipSide}
|
|
316
|
+
onClick={() => onSelect(item.value)}
|
|
317
|
+
/>
|
|
318
|
+
);
|
|
319
|
+
})}
|
|
320
|
+
</>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
@@ -15,6 +15,9 @@ interface Props {
|
|
|
15
15
|
sideOffset?: number;
|
|
16
16
|
side?: "top" | "bottom" | "left" | "right";
|
|
17
17
|
portalContainer?: HTMLElement | null;
|
|
18
|
+
/** Overrides the popup chrome entirely (e.g. a dark chip over a dark
|
|
19
|
+
* toolbar). The default light surface is used when omitted. */
|
|
20
|
+
contentClassName?: string;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export const Tooltip = React.memo(function Tooltip({
|
|
@@ -23,6 +26,7 @@ export const Tooltip = React.memo(function Tooltip({
|
|
|
23
26
|
sideOffset = 2,
|
|
24
27
|
side = "bottom",
|
|
25
28
|
portalContainer,
|
|
29
|
+
contentClassName,
|
|
26
30
|
}: Props) {
|
|
27
31
|
const portalScopeId = usePortalScopeId();
|
|
28
32
|
|
|
@@ -44,7 +48,10 @@ export const Tooltip = React.memo(function Tooltip({
|
|
|
44
48
|
className="n-z-menu"
|
|
45
49
|
>
|
|
46
50
|
<TooltipPrimitive.Popup
|
|
47
|
-
className={
|
|
51
|
+
className={
|
|
52
|
+
contentClassName ??
|
|
53
|
+
`${textStyles.small} n-text-text n-rounded-sm n-p-2 n-bg-popover-background n-shadow-tooltip n-z-menu`
|
|
54
|
+
}
|
|
48
55
|
>
|
|
49
56
|
{content}
|
|
50
57
|
</TooltipPrimitive.Popup>
|