@datalayer/primer-addons 1.0.6 → 1.0.7
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/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/toolbar/FloatingToolbar.d.ts +3 -0
- package/lib/components/toolbar/FloatingToolbar.js +126 -0
- package/lib/components/toolbar/Toolbar.d.ts +3 -0
- package/lib/components/toolbar/Toolbar.js +50 -0
- package/lib/components/toolbar/ToolbarButton.d.ts +8 -0
- package/lib/components/toolbar/ToolbarButton.js +70 -0
- package/lib/components/toolbar/ToolbarDivider.d.ts +6 -0
- package/lib/components/toolbar/ToolbarDivider.js +29 -0
- package/lib/components/toolbar/ToolbarDropdown.d.ts +8 -0
- package/lib/components/toolbar/ToolbarDropdown.js +41 -0
- package/lib/components/toolbar/ToolbarRenderer.d.ts +11 -0
- package/lib/components/toolbar/ToolbarRenderer.js +40 -0
- package/lib/components/toolbar/index.d.ts +7 -0
- package/lib/components/toolbar/index.js +11 -0
- package/lib/components/toolbar/types.d.ts +134 -0
- package/lib/components/toolbar/types.js +5 -0
- package/package.json +8 -5
package/lib/components/index.js
CHANGED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FloatingToolbarProps } from './types';
|
|
2
|
+
export declare function FloatingToolbar({ items, extraItems, anchorElement, isVisible, className, disabled, ariaLabel, portalContainer, }: FloatingToolbarProps): import("react").ReactPortal | null;
|
|
3
|
+
export default FloatingToolbar;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* FloatingToolbar - A floating toolbar that positions itself near a selection or anchor.
|
|
8
|
+
*
|
|
9
|
+
* Uses createPortal and absolute positioning to float above content.
|
|
10
|
+
* Designed to be used as a text-selection floating format bar, or an inline AI action bar.
|
|
11
|
+
*
|
|
12
|
+
* Extensible: consumers register items via the `items` and `extraItems` props.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <FloatingToolbar
|
|
17
|
+
* isVisible={hasSelection}
|
|
18
|
+
* anchorElement={floatingAnchor}
|
|
19
|
+
* items={formatItems}
|
|
20
|
+
* extraItems={aiItems}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module components/toolbar/FloatingToolbar
|
|
25
|
+
*/
|
|
26
|
+
import { useMemo, useRef, useCallback, useEffect } from 'react';
|
|
27
|
+
import { createPortal } from 'react-dom';
|
|
28
|
+
import { Box } from '../box/Box';
|
|
29
|
+
import { ToolbarRenderer } from './ToolbarRenderer';
|
|
30
|
+
/**
|
|
31
|
+
* Position the floating element relative to a DOMRect within an anchor element.
|
|
32
|
+
*/
|
|
33
|
+
function setFloatingPosition(targetRect, floatingElem, anchorElem, verticalGap = 10, horizontalOffset = 5) {
|
|
34
|
+
const scrollerElem = anchorElem.parentElement;
|
|
35
|
+
if (targetRect === null || !scrollerElem) {
|
|
36
|
+
floatingElem.style.opacity = '0';
|
|
37
|
+
floatingElem.style.transform = 'translateY(-4px)';
|
|
38
|
+
floatingElem.style.top = '-10000px';
|
|
39
|
+
floatingElem.style.left = '-10000px';
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const floatingElemRect = floatingElem.getBoundingClientRect();
|
|
43
|
+
const anchorElementRect = anchorElem.getBoundingClientRect();
|
|
44
|
+
const editorScrollerRect = scrollerElem.getBoundingClientRect();
|
|
45
|
+
let top = targetRect.top - floatingElemRect.height - verticalGap;
|
|
46
|
+
let left = targetRect.left -
|
|
47
|
+
horizontalOffset -
|
|
48
|
+
(anchorElementRect.left - editorScrollerRect.left);
|
|
49
|
+
// Keep within anchor bounds
|
|
50
|
+
if (top < anchorElementRect.top) {
|
|
51
|
+
top = targetRect.bottom + verticalGap;
|
|
52
|
+
}
|
|
53
|
+
if (left + floatingElemRect.width > editorScrollerRect.right) {
|
|
54
|
+
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
|
|
55
|
+
}
|
|
56
|
+
floatingElem.style.opacity = '1';
|
|
57
|
+
floatingElem.style.transform = 'translateY(0)';
|
|
58
|
+
floatingElem.style.top = `${top}px`;
|
|
59
|
+
floatingElem.style.left = `${left}px`;
|
|
60
|
+
}
|
|
61
|
+
export function FloatingToolbar({ items, extraItems, anchorElement, isVisible, className, disabled, ariaLabel = 'Floating toolbar', portalContainer, }) {
|
|
62
|
+
const floatingRef = useRef(null);
|
|
63
|
+
const allItems = useMemo(() => {
|
|
64
|
+
if (!extraItems?.length)
|
|
65
|
+
return items;
|
|
66
|
+
return [...items, ...extraItems];
|
|
67
|
+
}, [items, extraItems]);
|
|
68
|
+
const updatePosition = useCallback(() => {
|
|
69
|
+
const floatingElem = floatingRef.current;
|
|
70
|
+
if (!floatingElem || !anchorElement)
|
|
71
|
+
return;
|
|
72
|
+
const nativeSelection = window.getSelection();
|
|
73
|
+
if (!nativeSelection || nativeSelection.isCollapsed || !isVisible) {
|
|
74
|
+
floatingElem.style.opacity = '0';
|
|
75
|
+
floatingElem.style.transform = 'translateY(-4px)';
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const range = nativeSelection.getRangeAt(0);
|
|
79
|
+
const rangeRect = range.getBoundingClientRect();
|
|
80
|
+
setFloatingPosition(rangeRect, floatingElem, anchorElement);
|
|
81
|
+
}, [anchorElement, isVisible]);
|
|
82
|
+
// Update position on scroll and resize
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!isVisible)
|
|
85
|
+
return;
|
|
86
|
+
const update = () => updatePosition();
|
|
87
|
+
window.addEventListener('resize', update);
|
|
88
|
+
const scrollerElem = anchorElement?.parentElement;
|
|
89
|
+
if (scrollerElem) {
|
|
90
|
+
scrollerElem.addEventListener('scroll', update);
|
|
91
|
+
}
|
|
92
|
+
// Initial position
|
|
93
|
+
update();
|
|
94
|
+
return () => {
|
|
95
|
+
window.removeEventListener('resize', update);
|
|
96
|
+
if (scrollerElem) {
|
|
97
|
+
scrollerElem.removeEventListener('scroll', update);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}, [isVisible, updatePosition, anchorElement]);
|
|
101
|
+
if (!isVisible || allItems.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const container = portalContainer || anchorElement || document.body;
|
|
105
|
+
const toolbar = (_jsx(Box, { ref: floatingRef, className: className, role: "toolbar", "aria-label": ariaLabel, sx: {
|
|
106
|
+
display: 'flex',
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
gap: '2px',
|
|
109
|
+
px: 1,
|
|
110
|
+
py: '4px',
|
|
111
|
+
bg: 'canvas.overlay',
|
|
112
|
+
border: '1px solid',
|
|
113
|
+
borderColor: 'border.default',
|
|
114
|
+
borderRadius: 2,
|
|
115
|
+
boxShadow: 'shadow.large',
|
|
116
|
+
position: 'absolute',
|
|
117
|
+
zIndex: 10,
|
|
118
|
+
top: '-10000px',
|
|
119
|
+
left: '-10000px',
|
|
120
|
+
opacity: 0,
|
|
121
|
+
transition: 'opacity 0.15s ease, transform 0.15s ease',
|
|
122
|
+
transform: 'translateY(-4px)',
|
|
123
|
+
}, children: _jsx(ToolbarRenderer, { items: allItems, disabled: disabled, size: "small" }) }));
|
|
124
|
+
return createPortal(toolbar, container);
|
|
125
|
+
}
|
|
126
|
+
export default FloatingToolbar;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Toolbar - Fixed/sticky toolbar component.
|
|
8
|
+
*
|
|
9
|
+
* An extensible toolbar that renders ToolbarItems using Primer React components.
|
|
10
|
+
* Designed to be used as a rich text editor toolbar or any fixed toolbar.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <Toolbar
|
|
15
|
+
* items={[
|
|
16
|
+
* { key: 'bold', type: 'button', icon: BoldIcon, ariaLabel: 'Bold', onClick: () => {} },
|
|
17
|
+
* { key: 'divider-1', type: 'divider' },
|
|
18
|
+
* { key: 'heading', type: 'dropdown', ariaLabel: 'Heading', label: 'Normal', options: [...] },
|
|
19
|
+
* ]}
|
|
20
|
+
* extraItems={pluginItems}
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @module components/toolbar/Toolbar
|
|
25
|
+
*/
|
|
26
|
+
import { useMemo } from 'react';
|
|
27
|
+
import { Box } from '../box/Box';
|
|
28
|
+
import { ToolbarRenderer } from './ToolbarRenderer';
|
|
29
|
+
export function Toolbar({ items, extraItems, className, disabled, ariaLabel = 'Editor toolbar', }) {
|
|
30
|
+
const allItems = useMemo(() => {
|
|
31
|
+
if (!extraItems?.length)
|
|
32
|
+
return items;
|
|
33
|
+
return [...items, ...extraItems];
|
|
34
|
+
}, [items, extraItems]);
|
|
35
|
+
return (_jsx(Box, { className: className, role: "toolbar", "aria-label": ariaLabel, sx: {
|
|
36
|
+
display: 'flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
flexWrap: 'wrap',
|
|
39
|
+
gap: '2px',
|
|
40
|
+
px: 2,
|
|
41
|
+
py: 1,
|
|
42
|
+
bg: 'canvas.default',
|
|
43
|
+
borderBottom: '1px solid',
|
|
44
|
+
borderColor: 'border.default',
|
|
45
|
+
position: 'sticky',
|
|
46
|
+
top: 0,
|
|
47
|
+
zIndex: 2,
|
|
48
|
+
}, children: _jsx(ToolbarRenderer, { items: allItems, disabled: disabled, size: "medium" }) }));
|
|
49
|
+
}
|
|
50
|
+
export default Toolbar;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolbarButtonItem } from './types';
|
|
2
|
+
export interface ToolbarButtonProps {
|
|
3
|
+
item: ToolbarButtonItem;
|
|
4
|
+
/** Size variant: 'small' for floating toolbar, 'medium' for fixed toolbar */
|
|
5
|
+
size?: 'small' | 'medium';
|
|
6
|
+
}
|
|
7
|
+
export declare function ToolbarButton({ item, size }: ToolbarButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default ToolbarButton;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ToolbarButton - A single button in a Toolbar.
|
|
8
|
+
*
|
|
9
|
+
* Renders a native <button> with a direct Octicon icon.
|
|
10
|
+
* Avoids Primer's IconButton and Tooltip components to prevent invariant
|
|
11
|
+
* violations in Primer v37 (Tooltip requires a native interactive child,
|
|
12
|
+
* and IconButton's internal Tooltip conflicts with external wrappers).
|
|
13
|
+
*
|
|
14
|
+
* Uses the native HTML `title` attribute for tooltip behaviour.
|
|
15
|
+
*
|
|
16
|
+
* @module components/toolbar/ToolbarButton
|
|
17
|
+
*/
|
|
18
|
+
import { isValidElement } from 'react';
|
|
19
|
+
export function ToolbarButton({ item, size = 'medium' }) {
|
|
20
|
+
const { ariaLabel, title, icon, label, isActive, onClick, disabled } = item;
|
|
21
|
+
// Resolve icon → React element.
|
|
22
|
+
// Octicon components use React.forwardRef, which returns an *object*
|
|
23
|
+
// (typeof === 'object'), NOT a function. So we check isValidElement first
|
|
24
|
+
// (already-instantiated JSX), then treat anything else as a component type.
|
|
25
|
+
let iconElement = null;
|
|
26
|
+
if (icon) {
|
|
27
|
+
if (isValidElement(icon)) {
|
|
28
|
+
iconElement = icon;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Component type: function component, forwardRef, or memo wrapper
|
|
32
|
+
const IconComp = icon;
|
|
33
|
+
iconElement = _jsx(IconComp, { size: 16 });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (label) {
|
|
37
|
+
// Fallback: render text label (e.g. "x₂" for subscript)
|
|
38
|
+
iconElement = _jsx("span", { style: { fontSize: 12, fontWeight: 600, lineHeight: 1 }, children: label });
|
|
39
|
+
}
|
|
40
|
+
const btnSize = size === 'small' ? 28 : 32;
|
|
41
|
+
return (_jsx("button", { type: "button", "aria-label": ariaLabel, title: title, onClick: onClick, disabled: disabled, style: {
|
|
42
|
+
display: 'inline-flex',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
width: btnSize,
|
|
46
|
+
height: btnSize,
|
|
47
|
+
padding: 0,
|
|
48
|
+
margin: 0,
|
|
49
|
+
border: 'none',
|
|
50
|
+
borderRadius: 6,
|
|
51
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
52
|
+
background: isActive ? 'var(--bgColor-accent-muted, rgba(9,105,218,0.1))' : 'transparent',
|
|
53
|
+
color: isActive ? 'var(--fgColor-accent, #0969da)' : 'var(--fgColor-muted, #656d76)',
|
|
54
|
+
opacity: disabled ? 0.5 : 1,
|
|
55
|
+
lineHeight: 1,
|
|
56
|
+
}, onMouseEnter: (e) => {
|
|
57
|
+
if (!disabled) {
|
|
58
|
+
e.currentTarget.style.background = 'var(--bgColor-neutral-muted, rgba(175,184,193,0.2))';
|
|
59
|
+
e.currentTarget.style.color = 'var(--fgColor-default, #1f2328)';
|
|
60
|
+
}
|
|
61
|
+
}, onMouseLeave: (e) => {
|
|
62
|
+
e.currentTarget.style.background = isActive
|
|
63
|
+
? 'var(--bgColor-accent-muted, rgba(9,105,218,0.1))'
|
|
64
|
+
: 'transparent';
|
|
65
|
+
e.currentTarget.style.color = isActive
|
|
66
|
+
? 'var(--fgColor-accent, #0969da)'
|
|
67
|
+
: 'var(--fgColor-muted, #656d76)';
|
|
68
|
+
}, children: iconElement }));
|
|
69
|
+
}
|
|
70
|
+
export default ToolbarButton;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface ToolbarDividerProps {
|
|
2
|
+
/** Orientation: vertical for horizontal toolbars, horizontal for vertical */
|
|
3
|
+
orientation?: 'vertical' | 'horizontal';
|
|
4
|
+
}
|
|
5
|
+
export declare function ToolbarDivider({ orientation }: ToolbarDividerProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export default ToolbarDivider;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ToolbarDivider - A vertical divider between toolbar items.
|
|
8
|
+
*
|
|
9
|
+
* @module components/toolbar/ToolbarDivider
|
|
10
|
+
*/
|
|
11
|
+
import { Box } from '../box/Box';
|
|
12
|
+
export function ToolbarDivider({ orientation = 'vertical' }) {
|
|
13
|
+
return (_jsx(Box, { sx: {
|
|
14
|
+
...(orientation === 'vertical'
|
|
15
|
+
? {
|
|
16
|
+
width: '1px',
|
|
17
|
+
height: '20px',
|
|
18
|
+
mx: 1,
|
|
19
|
+
}
|
|
20
|
+
: {
|
|
21
|
+
height: '1px',
|
|
22
|
+
width: '100%',
|
|
23
|
+
my: 1,
|
|
24
|
+
}),
|
|
25
|
+
bg: 'border.muted',
|
|
26
|
+
flexShrink: 0,
|
|
27
|
+
} }));
|
|
28
|
+
}
|
|
29
|
+
export default ToolbarDivider;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolbarDropdownItem } from './types';
|
|
2
|
+
export interface ToolbarDropdownProps {
|
|
3
|
+
item: ToolbarDropdownItem;
|
|
4
|
+
/** Size variant */
|
|
5
|
+
size?: 'small' | 'medium';
|
|
6
|
+
}
|
|
7
|
+
export declare function ToolbarDropdown({ item, size }: ToolbarDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default ToolbarDropdown;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ToolbarDropdown - A dropdown menu in a Toolbar.
|
|
8
|
+
*
|
|
9
|
+
* Uses Primer React ActionMenu for accessible dropdown menus.
|
|
10
|
+
*
|
|
11
|
+
* @module components/toolbar/ToolbarDropdown
|
|
12
|
+
*/
|
|
13
|
+
import { isValidElement } from 'react';
|
|
14
|
+
import { ActionMenu, ActionList, Text } from '@primer/react';
|
|
15
|
+
import { Box } from '../box/Box';
|
|
16
|
+
function renderIcon(icon) {
|
|
17
|
+
if (!icon)
|
|
18
|
+
return null;
|
|
19
|
+
if (typeof icon === 'function') {
|
|
20
|
+
const IconComp = icon;
|
|
21
|
+
return _jsx(IconComp, { size: 16 });
|
|
22
|
+
}
|
|
23
|
+
if (isValidElement(icon)) {
|
|
24
|
+
return icon;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function ToolbarDropdown({ item, size = 'medium' }) {
|
|
29
|
+
const { ariaLabel, icon, label, options, disabled } = item;
|
|
30
|
+
return (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { "aria-label": ariaLabel, disabled: disabled, variant: "invisible", size: size, sx: {
|
|
31
|
+
color: 'fg.muted',
|
|
32
|
+
fontWeight: 'normal',
|
|
33
|
+
fontSize: size === 'small' ? 0 : 1,
|
|
34
|
+
px: 2,
|
|
35
|
+
'&:hover:not([disabled])': {
|
|
36
|
+
bg: 'neutral.muted',
|
|
37
|
+
color: 'fg.default',
|
|
38
|
+
},
|
|
39
|
+
}, children: _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 }, children: [renderIcon(icon), label && (_jsx(Text, { sx: { fontSize: size === 'small' ? 0 : 1 }, children: label }))] }) }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsx(ActionList, { children: options.map(option => (_jsxs(ActionList.Item, { onSelect: option.onClick, disabled: option.disabled, active: option.isActive, children: [option.icon && (_jsx(ActionList.LeadingVisual, { children: renderIcon(option.icon) })), _jsxs(Box, { sx: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: 3 }, children: [_jsx(Text, { children: option.label }), option.shortcut && (_jsx(Text, { sx: { color: 'fg.subtle', fontSize: 0, fontFamily: 'mono' }, children: option.shortcut }))] })] }, option.key))) }) })] }));
|
|
40
|
+
}
|
|
41
|
+
export default ToolbarDropdown;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ToolbarItem } from './types';
|
|
2
|
+
export interface ToolbarRendererProps {
|
|
3
|
+
items: ToolbarItem[];
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
size?: 'small' | 'medium';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sort items by order, then render each one according to its type.
|
|
9
|
+
*/
|
|
10
|
+
export declare function ToolbarRenderer({ items, disabled, size }: ToolbarRendererProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export default ToolbarRenderer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ToolbarRenderer - Renders an array of ToolbarItems.
|
|
8
|
+
*
|
|
9
|
+
* Shared logic between Toolbar and FloatingToolbar.
|
|
10
|
+
*
|
|
11
|
+
* @module components/toolbar/ToolbarRenderer
|
|
12
|
+
*/
|
|
13
|
+
import { Fragment } from 'react';
|
|
14
|
+
import { ToolbarButton } from './ToolbarButton';
|
|
15
|
+
import { ToolbarDropdown } from './ToolbarDropdown';
|
|
16
|
+
import { ToolbarDivider } from './ToolbarDivider';
|
|
17
|
+
/**
|
|
18
|
+
* Sort items by order, then render each one according to its type.
|
|
19
|
+
*/
|
|
20
|
+
export function ToolbarRenderer({ items, disabled, size = 'medium' }) {
|
|
21
|
+
const sorted = [...items]
|
|
22
|
+
.filter(item => !item.hidden)
|
|
23
|
+
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
24
|
+
return (_jsx(_Fragment, { children: sorted.map(item => {
|
|
25
|
+
const itemDisabled = disabled || item.disabled;
|
|
26
|
+
switch (item.type) {
|
|
27
|
+
case 'button':
|
|
28
|
+
return (_jsx(Fragment, { children: _jsx(ToolbarButton, { item: { ...item, disabled: itemDisabled }, size: size }) }, item.key));
|
|
29
|
+
case 'dropdown':
|
|
30
|
+
return (_jsx(Fragment, { children: _jsx(ToolbarDropdown, { item: { ...item, disabled: itemDisabled }, size: size }) }, item.key));
|
|
31
|
+
case 'divider':
|
|
32
|
+
return (_jsx(Fragment, { children: _jsx(ToolbarDivider, {}) }, item.key));
|
|
33
|
+
case 'custom':
|
|
34
|
+
return _jsx(Fragment, { children: item.render() }, item.key);
|
|
35
|
+
default:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}) }));
|
|
39
|
+
}
|
|
40
|
+
export default ToolbarRenderer;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
export * from './types';
|
|
6
|
+
export * from './Toolbar';
|
|
7
|
+
export * from './FloatingToolbar';
|
|
8
|
+
export * from './ToolbarButton';
|
|
9
|
+
export * from './ToolbarDropdown';
|
|
10
|
+
export * from './ToolbarDivider';
|
|
11
|
+
export * from './ToolbarRenderer';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { IconProps } from '@primer/octicons-react';
|
|
3
|
+
/**
|
|
4
|
+
* Toolbar item types that can be registered in a toolbar.
|
|
5
|
+
*/
|
|
6
|
+
export type ToolbarItemType = 'button' | 'dropdown' | 'divider' | 'custom';
|
|
7
|
+
/**
|
|
8
|
+
* Base toolbar item definition.
|
|
9
|
+
*/
|
|
10
|
+
export interface ToolbarItemBase {
|
|
11
|
+
/** Unique key for the item */
|
|
12
|
+
key: string;
|
|
13
|
+
/** Item type */
|
|
14
|
+
type: ToolbarItemType;
|
|
15
|
+
/** Sort order (lower = earlier). Default: 100 */
|
|
16
|
+
order?: number;
|
|
17
|
+
/** Group name for logical grouping */
|
|
18
|
+
group?: string;
|
|
19
|
+
/** Whether the item is disabled */
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
/** Whether to hide the item */
|
|
22
|
+
hidden?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Toolbar button item definition.
|
|
26
|
+
*/
|
|
27
|
+
export interface ToolbarButtonItem extends ToolbarItemBase {
|
|
28
|
+
type: 'button';
|
|
29
|
+
/** Accessible label */
|
|
30
|
+
ariaLabel: string;
|
|
31
|
+
/** Tooltip text */
|
|
32
|
+
title?: string;
|
|
33
|
+
/** Icon component (Octicon or custom) */
|
|
34
|
+
icon?: React.ComponentType<IconProps> | ReactNode;
|
|
35
|
+
/** Text label */
|
|
36
|
+
label?: string;
|
|
37
|
+
/** Whether the button is in an active/pressed state */
|
|
38
|
+
isActive?: boolean;
|
|
39
|
+
/** Click handler */
|
|
40
|
+
onClick: () => void;
|
|
41
|
+
/** Keyboard shortcut hint (e.g., "⌘B") */
|
|
42
|
+
shortcut?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Dropdown option for ToolbarDropdownItem.
|
|
46
|
+
*/
|
|
47
|
+
export interface ToolbarDropdownOption {
|
|
48
|
+
/** Unique key */
|
|
49
|
+
key: string;
|
|
50
|
+
/** Display label */
|
|
51
|
+
label: string;
|
|
52
|
+
/** Icon component or element */
|
|
53
|
+
icon?: React.ComponentType<IconProps> | ReactNode;
|
|
54
|
+
/** Whether this option is active/selected */
|
|
55
|
+
isActive?: boolean;
|
|
56
|
+
/** Click handler */
|
|
57
|
+
onClick: () => void;
|
|
58
|
+
/** Keyboard shortcut hint */
|
|
59
|
+
shortcut?: string;
|
|
60
|
+
/** Whether the option is disabled */
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Toolbar dropdown item definition.
|
|
65
|
+
*/
|
|
66
|
+
export interface ToolbarDropdownItem extends ToolbarItemBase {
|
|
67
|
+
type: 'dropdown';
|
|
68
|
+
/** Accessible label */
|
|
69
|
+
ariaLabel: string;
|
|
70
|
+
/** Tooltip text */
|
|
71
|
+
title?: string;
|
|
72
|
+
/** Icon component or element for the trigger */
|
|
73
|
+
icon?: React.ComponentType<IconProps> | ReactNode;
|
|
74
|
+
/** Text label for the trigger */
|
|
75
|
+
label?: string;
|
|
76
|
+
/** Dropdown options */
|
|
77
|
+
options: ToolbarDropdownOption[];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Toolbar divider item definition.
|
|
81
|
+
*/
|
|
82
|
+
export interface ToolbarDividerItem extends ToolbarItemBase {
|
|
83
|
+
type: 'divider';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Toolbar custom item definition.
|
|
87
|
+
*/
|
|
88
|
+
export interface ToolbarCustomItem extends ToolbarItemBase {
|
|
89
|
+
type: 'custom';
|
|
90
|
+
/** Custom render function */
|
|
91
|
+
render: () => ReactNode;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Union of all toolbar item types.
|
|
95
|
+
*/
|
|
96
|
+
export type ToolbarItem = ToolbarButtonItem | ToolbarDropdownItem | ToolbarDividerItem | ToolbarCustomItem;
|
|
97
|
+
/**
|
|
98
|
+
* Props for the extensible Toolbar component.
|
|
99
|
+
*/
|
|
100
|
+
export interface ToolbarProps {
|
|
101
|
+
/** Array of toolbar items to render */
|
|
102
|
+
items: ToolbarItem[];
|
|
103
|
+
/** Additional CSS class name */
|
|
104
|
+
className?: string;
|
|
105
|
+
/** Whether the entire toolbar is disabled */
|
|
106
|
+
disabled?: boolean;
|
|
107
|
+
/** Additional items to append (for extensibility) */
|
|
108
|
+
extraItems?: ToolbarItem[];
|
|
109
|
+
/** Aria label for the toolbar */
|
|
110
|
+
ariaLabel?: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Props for the extensible FloatingToolbar component.
|
|
114
|
+
*/
|
|
115
|
+
export interface FloatingToolbarProps {
|
|
116
|
+
/** Array of toolbar items to render */
|
|
117
|
+
items: ToolbarItem[];
|
|
118
|
+
/** The anchor element to position relative to */
|
|
119
|
+
anchorElement?: HTMLElement | null;
|
|
120
|
+
/** Whether the floating toolbar is visible */
|
|
121
|
+
isVisible: boolean;
|
|
122
|
+
/** Additional CSS class name */
|
|
123
|
+
className?: string;
|
|
124
|
+
/** Whether the entire toolbar is disabled */
|
|
125
|
+
disabled?: boolean;
|
|
126
|
+
/** Additional items to append (for extensibility) */
|
|
127
|
+
extraItems?: ToolbarItem[];
|
|
128
|
+
/** Aria label for the toolbar */
|
|
129
|
+
ariaLabel?: string;
|
|
130
|
+
/** Callback when the toolbar requests to close */
|
|
131
|
+
onClose?: () => void;
|
|
132
|
+
/** Portal container element. Defaults to document.body */
|
|
133
|
+
portalContainer?: HTMLElement;
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalayer/primer-addons",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"dev": "vite",
|
|
7
|
-
"start": "vite",
|
|
8
6
|
"build": "tsc && vite build",
|
|
9
|
-
"
|
|
7
|
+
"build-storybook": "storybook build",
|
|
8
|
+
"build:lib": "npm run clean:lib&& tsc -b",
|
|
9
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
10
|
+
"dev": "vite",
|
|
10
11
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
11
12
|
"preview": "vite preview",
|
|
13
|
+
"start": "vite",
|
|
12
14
|
"storybook": "storybook dev -p 6006",
|
|
13
|
-
"
|
|
15
|
+
"watch": "tsc -w",
|
|
16
|
+
"watch:lib": "tsc -b -w"
|
|
14
17
|
},
|
|
15
18
|
"main": "lib/index.js",
|
|
16
19
|
"files": [
|