@ultraviolet/plus 0.14.3 → 0.15.0

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, MouseEventHandler, ComponentProps, RefObject } from 'react';
2
+ import { ReactNode, MouseEventHandler, ComponentProps, RefObject, Dispatch } from 'react';
3
3
  import * as _emotion_react_jsx_runtime from '@emotion/react/jsx-runtime';
4
4
  import { langs } from '@uiw/codemirror-extensions-langs';
5
5
  import CodeMirror from '@uiw/react-codemirror';
@@ -455,11 +455,12 @@ declare const Navigation: {
455
455
  * some part of your logo
456
456
  */
457
457
  }) => _emotion_react_jsx_runtime.JSX.Element | null;
458
- Item: ({ children, categoryIcon, categoryIconVariant, label, subLabel, badgeText, badgeSentiment, href, onClick, toggle, active, noPinButton, type, hasParents, as, disabled, noExpand, toggleMenu, }: {
458
+ Item: ({ children, categoryIcon, categoryIconVariant, label, subLabel, badgeText, badgeSentiment, href, onClick, toggle, active, noPinButton, type, hasParents, as, disabled, noExpand, index, id, }: {
459
459
  children?: ReactNode;
460
460
  categoryIcon?: "security" | "console" | "database" | "pin" | "billing" | "storage" | "baremetal" | "webHosting" | "vpc" | "useCase" | "toolsServices" | "serverless" | "observability" | "network" | "managedServices" | "iot" | "documentation" | "dedicatedServer" | "datacenter" | "containers" | "compute" | "ai" | "labs" | "devTools" | "applicationIntegration" | undefined;
461
461
  categoryIconVariant?: "neutral" | "primary" | undefined;
462
462
  label: string;
463
+ id: string;
463
464
  subLabel?: string | undefined;
464
465
  badgeText?: string | undefined;
465
466
  badgeSentiment?: ("neutral" | "danger" | "info" | "primary" | "secondary" | "success" | "warning") | undefined;
@@ -469,24 +470,34 @@ declare const Navigation: {
469
470
  active?: boolean | undefined;
470
471
  noPinButton?: boolean | undefined;
471
472
  type?: ("default" | "pinned" | "pinnedGroup") | undefined;
472
- toggleMenu?: (() => void) | undefined;
473
473
  hasParents?: boolean | undefined;
474
+ index?: number | undefined;
474
475
  as?: keyof react.JSX.IntrinsicElements | undefined;
475
476
  noExpand?: boolean | undefined;
476
477
  disabled?: boolean | undefined;
477
478
  }) => _emotion_react_jsx_runtime.JSX.Element | null;
478
- PinnedItems: () => _emotion_react_jsx_runtime.JSX.Element | null;
479
+ PinnedItems: ({ toggle }: {
480
+ toggle?: boolean | undefined;
481
+ }) => _emotion_react_jsx_runtime.JSX.Element | null;
479
482
  Separator: () => _emotion_react_jsx_runtime.JSX.Element;
480
483
  };
481
484
 
482
485
  declare const _default: {
483
486
  readonly 'navigation.pin.tooltip': "Pin product";
484
487
  readonly 'navigation.unpin.tooltip': "Unpin product";
488
+ readonly 'navigation.pin.limit': "You cannot pin more products";
485
489
  readonly 'navigation.pinned.item.group.label': "Pinned items";
486
490
  readonly 'navigation.expand.button': "Expand sidebar";
487
491
  readonly 'navigation.collapse.button': "Collapse sidebar";
492
+ readonly 'navigation.pinned.item.group.empty': "You have no pinned items.";
488
493
  };
489
494
 
495
+ type Item = {
496
+ label: string;
497
+ active?: boolean;
498
+ onClick?: (toggle?: true | false) => void;
499
+ };
500
+ type Items = Record<string, Item>;
490
501
  type ContextProps = {
491
502
  expanded: boolean;
492
503
  toggleExpand: (toggle?: boolean) => void;
@@ -501,6 +512,21 @@ type ContextProps = {
501
512
  locales: typeof _default;
502
513
  width: number;
503
514
  setWidth: (width: number) => void;
515
+ /**
516
+ * This function will reorder the pinned items based on the initial index and
517
+ * the end index.
518
+ */
519
+ reorderItems: (
520
+ /**
521
+ * The initial index of the item
522
+ */
523
+ initialIndex: number,
524
+ /**
525
+ * The end index of the item
526
+ */
527
+ endIndex: number) => void;
528
+ items: Items;
529
+ registerItem: Dispatch<Items>;
504
530
  };
505
531
  declare const useNavigation: () => ContextProps;
506
532
  type NavigationProviderProps = {
@@ -526,11 +552,6 @@ type NavigationProviderProps = {
526
552
  * navigation will be expanded by default otherwise it will be collapsed
527
553
  */
528
554
  initialExpanded?: boolean;
529
- /**
530
- * This function is triggered when the user click on the pin/unpin button
531
- * of an item
532
- */
533
- onClickPinUnpin?: (pinned: string[]) => void;
534
555
  locales?: typeof _default;
535
556
  /**
536
557
  * This function is triggered when user click on expand button on the footer
@@ -538,8 +559,22 @@ type NavigationProviderProps = {
538
559
  * and it automatically collapse / expand.
539
560
  */
540
561
  onClickExpand?: (expanded: boolean) => void;
562
+ /**
563
+ * This function is triggered when the user click on the pin/unpin button
564
+ * of an item. To access all pinned item you can use the `useNavigation` hook
565
+ * and access the `pinnedItems` property.
566
+ */
567
+ onClickPinUnpin?: (
568
+ /**
569
+ * The state of the item after the click
570
+ */
571
+ state: 'pin' | 'unpin',
572
+ /**
573
+ * The current items that has been pinned on click
574
+ */
575
+ pinned: string) => void;
541
576
  };
542
- declare const NavigationProvider: ({ children, pinnedFeature, onClickPinUnpin, initialPinned, initialExpanded, locales, pinLimit, onClickExpand, initialWidth, }: NavigationProviderProps) => _emotion_react_jsx_runtime.JSX.Element;
577
+ declare const NavigationProvider: ({ children, pinnedFeature, initialPinned, initialExpanded, locales, pinLimit, onClickExpand, initialWidth, onClickPinUnpin, }: NavigationProviderProps) => _emotion_react_jsx_runtime.JSX.Element;
543
578
 
544
579
  type FAQProps = {
545
580
  description: string;
@@ -1,4 +1,4 @@
1
- import { useContext, useState, useRef, useCallback, useMemo, createContext } from 'react';
1
+ import { useContext, useState, useReducer, useRef, useCallback, useMemo, createContext } from 'react';
2
2
  import { ANIMATION_DURATION, NAVIGATION_WIDTH } from './constants.js';
3
3
  import NavigationLocales from './locales/en.js';
4
4
  import { jsx } from '@emotion/react/jsx-runtime';
@@ -20,24 +20,34 @@ const NavigationContext = /*#__PURE__*/createContext({
20
20
  current: null
21
21
  },
22
22
  width: NAVIGATION_WIDTH,
23
- setWidth: () => {}
23
+ setWidth: () => {},
24
+ reorderItems: () => {},
25
+ items: {},
26
+ registerItem: () => {}
24
27
  });
25
28
  const useNavigation = () => useContext(NavigationContext);
26
29
  const NavigationProvider = ({
27
30
  children,
28
31
  pinnedFeature = false,
29
- onClickPinUnpin,
30
32
  initialPinned,
31
33
  initialExpanded = true,
32
34
  locales = NavigationLocales,
33
35
  pinLimit = 7,
34
36
  onClickExpand,
35
- initialWidth = NAVIGATION_WIDTH
37
+ initialWidth = NAVIGATION_WIDTH,
38
+ onClickPinUnpin
36
39
  }) => {
37
40
  const [expanded, setExpanded] = useState(initialExpanded);
38
41
  const [pinnedItems, setPinnedItems] = useState(initialPinned ?? []);
39
42
  const [animation, setAnimation] = useState(false);
40
43
  const [width, setWidth] = useState(initialWidth);
44
+
45
+ // This is used to store the items that are registered in the navigation
46
+ // This way we can retrieve items with their active state in pinned feature
47
+ const [items, registerItem] = useReducer((oldState, newState) => ({
48
+ ...oldState,
49
+ ...newState
50
+ }), {});
41
51
  const navigationRef = useRef(null);
42
52
 
43
53
  // This function will be triggered when expand/collapse button is clicked
@@ -60,12 +70,18 @@ const NavigationProvider = ({
60
70
  }, [expanded, onClickExpand, setAnimation, setExpanded]);
61
71
  const pinItem = useCallback(item => {
62
72
  setPinnedItems([...pinnedItems, item]);
63
- onClickPinUnpin?.(pinnedItems);
73
+ onClickPinUnpin?.('pin', item);
64
74
  }, [onClickPinUnpin, pinnedItems]);
65
75
  const unpinItem = useCallback(item => {
66
76
  setPinnedItems(pinnedItems.filter(localItem => localItem !== item));
67
- onClickPinUnpin?.(pinnedItems);
77
+ onClickPinUnpin?.('unpin', item);
68
78
  }, [onClickPinUnpin, pinnedItems]);
79
+ const reorderItems = useCallback((initialIndex, endIndex) => {
80
+ const newPinnedItems = [...pinnedItems];
81
+ const [removed] = newPinnedItems.splice(initialIndex, 1);
82
+ newPinnedItems.splice(endIndex, 0, removed);
83
+ setPinnedItems(newPinnedItems);
84
+ }, [pinnedItems]);
69
85
  const value = useMemo(() => ({
70
86
  expanded,
71
87
  toggleExpand,
@@ -79,8 +95,11 @@ const NavigationProvider = ({
79
95
  setAnimation,
80
96
  navigationRef,
81
97
  width,
82
- setWidth
83
- }), [expanded, toggleExpand, pinnedItems, pinItem, unpinItem, pinnedFeature, locales, pinLimit, animation, width]);
98
+ setWidth,
99
+ reorderItems,
100
+ registerItem,
101
+ items
102
+ }), [expanded, toggleExpand, pinnedItems, pinItem, unpinItem, pinnedFeature, locales, pinLimit, animation, width, reorderItems, items]);
84
103
  return jsx(NavigationContext.Provider, {
85
104
  value: value,
86
105
  children: children
@@ -9,6 +9,8 @@ const StyledText = /*#__PURE__*/_styled(Text, {
9
9
  target: "eh4zgrv2"
10
10
  })("padding-bottom:", ({
11
11
  theme
12
+ }) => theme.space['1'], ";padding-left:", ({
13
+ theme
12
14
  }) => theme.space['1'], ";transition:opacity ", ANIMATION_DURATION, "ms ease-in-out,height ", ANIMATION_DURATION, "ms ease-in-out;height:", ({
13
15
  theme
14
16
  }) => `calc(${theme.typography.bodySmallStrong.lineHeight} + ${theme.space['1']})`, ";");
@@ -1,9 +1,9 @@
1
1
  import _styled from '@emotion/styled/base';
2
2
  import '@emotion/react';
3
- import { MenuV2, Stack, Tooltip, Expandable, Button, Badge, Text, fadeIn } from '@ultraviolet/ui';
4
- import { useState, useCallback, useEffect, useMemo, Children, isValidElement, cloneElement } from 'react';
3
+ import { Button, MenuV2, Stack, Tooltip, Expandable, Badge, Text, fadeIn } from '@ultraviolet/ui';
4
+ import { useEffect, useState, useCallback, useMemo, Children, isValidElement, cloneElement } from 'react';
5
5
  import { useNavigation } from '../NavigationProvider.js';
6
- import { ANIMATION_DURATION, PIN_BUTTON_OPACITY_TRANSITION, shrinkHeight } from '../constants.js';
6
+ import { ANIMATION_DURATION, shrinkHeight } from '../constants.js';
7
7
  import { jsxs, Fragment, jsx } from '@emotion/react/jsx-runtime';
8
8
  import { CategoryIcon } from '../../../../@ultraviolet/icons/dist/components/CategoryIcon/index.js';
9
9
  import { Icon } from '../../../../@ultraviolet/icons/dist/components/Icon/index.js';
@@ -17,17 +17,46 @@ const NeutralButtonLink = process.env.NODE_ENV === "production" ? {
17
17
  styles: "color:inherit;text-decoration:none;background-color:inherit;border:none;text-align:left",
18
18
  toString: _EMOTION_STRINGIFIED_CSS_ERROR__
19
19
  };
20
+
21
+ // Pin button when the navigation is expanded
20
22
  const ExpandedPinnedButton = /*#__PURE__*/_styled(Button, {
23
+ target: "e134hokc12"
24
+ })(process.env.NODE_ENV === "production" ? {
25
+ name: "9hkyle",
26
+ styles: "opacity:0;right:0;position:absolute;left:-24px;top:0;bottom:0;margin:auto;&:hover,&:focus,&:active{opacity:1;}"
27
+ } : {
28
+ name: "9hkyle",
29
+ styles: "opacity:0;right:0;position:absolute;left:-24px;top:0;bottom:0;margin:auto;&:hover,&:focus,&:active{opacity:1;}",
30
+ toString: _EMOTION_STRINGIFIED_CSS_ERROR__
31
+ });
32
+ const GrabIcon = /*#__PURE__*/_styled(Icon, {
21
33
  target: "e134hokc11"
22
- })("opacity:0;right:0;position:absolute;left:-24px;top:0;bottom:0;margin:auto;&:hover,&:focus{opacity:1;}transition:", PIN_BUTTON_OPACITY_TRANSITION, ";transition-delay:0.3s;");
34
+ })("opacity:0;margin:0 ", ({
35
+ theme
36
+ }) => theme.space['0.25'], ";cursor:grab;");
37
+
38
+ // Pin button when the navigation is collapsed
23
39
  const CollapsedPinnedButton = /*#__PURE__*/_styled(Button, {
24
40
  target: "e134hokc10"
25
- })("opacity:0;&:hover,&:focus{opacity:1;}transition:", PIN_BUTTON_OPACITY_TRANSITION, ";transition-delay:0.3s;");
26
- const StyledMenuItem = /*#__PURE__*/_styled(MenuV2.Item, {
41
+ })(process.env.NODE_ENV === "production" ? {
42
+ name: "i8vgdw",
43
+ styles: "position:absolute;opacity:0;right:0;margin:auto;top:0;bottom:0;&:hover{opacity:1;}"
44
+ } : {
45
+ name: "i8vgdw",
46
+ styles: "position:absolute;opacity:0;right:0;margin:auto;top:0;bottom:0;&:hover{opacity:1;}",
47
+ toString: _EMOTION_STRINGIFIED_CSS_ERROR__
48
+ });
49
+ const StyledBadge = /*#__PURE__*/_styled(Badge, {
27
50
  target: "e134hokc9"
28
- })("text-align:left;&:hover{", ExpandedPinnedButton, ",", CollapsedPinnedButton, "{opacity:1;}}");
29
- const StyledMenu = /*#__PURE__*/_styled(MenuV2, {
51
+ })();
52
+ const StyledMenuItem = /*#__PURE__*/_styled(MenuV2.Item, {
53
+ shouldForwardProp: prop => !['isPinnable'].includes(prop),
30
54
  target: "e134hokc8"
55
+ })("text-align:left;&:hover,&:focus,&:active{", CollapsedPinnedButton, "{opacity:1;}", StyledBadge, "{opacity:", ({
56
+ isPinnable
57
+ }) => isPinnable ? 0 : 1, ";}}");
58
+ const StyledMenu = /*#__PURE__*/_styled(MenuV2, {
59
+ target: "e134hokc7"
31
60
  })(process.env.NODE_ENV === "production" ? {
32
61
  name: "educr3",
33
62
  styles: "width:180px"
@@ -36,9 +65,6 @@ const StyledMenu = /*#__PURE__*/_styled(MenuV2, {
36
65
  styles: "width:180px",
37
66
  toString: _EMOTION_STRINGIFIED_CSS_ERROR__
38
67
  });
39
- const StyledBadge = /*#__PURE__*/_styled(Badge, {
40
- target: "e134hokc7"
41
- })("transition:", PIN_BUTTON_OPACITY_TRANSITION, ";transition-delay:0.3s;");
42
68
  const PaddingStack = /*#__PURE__*/_styled(Stack, {
43
69
  target: "e134hokc6"
44
70
  })(process.env.NODE_ENV === "production" ? {
@@ -80,11 +106,13 @@ const StyledContainer = /*#__PURE__*/_styled(Stack, {
80
106
  theme
81
107
  }) => `calc(${theme.space['0.25']} + ${theme.space['0.5']}) ${theme.space['1']}`, ";&[data-has-sub-label='true']{padding:", ({
82
108
  theme
83
- }) => `${theme.space['0.5']} ${theme.space['1']}`, ";}width:100%;&:hover[data-has-no-expand='false']:not([disabled]):not(\n [data-is-active='true']\n ),&:focus[data-has-no-expand='false']:not([disabled]):not(\n [data-is-active='true']\n ),&[data-has-active-children='true'][data-has-no-expand='false']:not(\n [disabled][data-is-active='true']\n ){background-color:", ({
109
+ }) => `${theme.space['0.5']} ${theme.space['1']}`, ";}width:100%;&:hover[data-has-no-expand='false']:not([disabled]):not(\n [data-is-active='true']\n ),&:focus[data-has-no-expand='false']:not([disabled]):not(\n [data-is-active='true']\n ){background-color:", ({
84
110
  theme
85
- }) => theme.colors.neutral.backgroundHover, ";", WrapText, "{color:", ({
111
+ }) => theme.colors.neutral.backgroundWeak, ";}&[data-has-active-children='true'][data-has-no-expand='false']:not(\n [disabled][data-is-active='true']\n ){background-color:", ({
86
112
  theme
87
- }) => theme.colors.neutral.textWeakHover, ";}", ExpandedPinnedButton, ",", CollapsedPinnedButton, "{opacity:1;}&[data-is-pinnable='true']{", StyledBadge, "{opacity:0;}}}&:hover[data-has-children='false'][data-is-active='false']{", WrapText, "{color:", ({
113
+ }) => theme.colors.neutral.backgroundWeakHover, ";", WrapText, "{color:", ({
114
+ theme
115
+ }) => theme.colors.neutral.textWeakHover, ";}", ExpandedPinnedButton, ",", CollapsedPinnedButton, "{opacity:1;}&[data-is-pinnable='true']{", StyledBadge, "{opacity:0;}}}&[data-has-no-expand='false']:not([disabled]){&:hover,&:focus,&:active{", ExpandedPinnedButton, ",", CollapsedPinnedButton, ",", GrabIcon, "{opacity:1;}", StyledBadge, "{opacity:0;}}}&:hover[data-has-children='false'][data-is-active='false']:not([disabled]){", WrapText, "{color:", ({
88
116
  theme
89
117
  }) => theme.colors.neutral.textWeakHover, ";}}&:active[data-has-no-expand='false']:not([disabled]):not(\n [data-is-active='true']\n ){background-color:", ({
90
118
  theme
@@ -130,7 +158,8 @@ const Item = ({
130
158
  as,
131
159
  disabled,
132
160
  noExpand = false,
133
- toggleMenu
161
+ index,
162
+ id
134
163
  }) => {
135
164
  const context = useNavigation();
136
165
  if (!context) {
@@ -144,8 +173,22 @@ const Item = ({
144
173
  unpinItem,
145
174
  pinnedItems,
146
175
  pinLimit,
147
- animation
176
+ animation,
177
+ registerItem
148
178
  } = context;
179
+ useEffect(() => {
180
+ if (type !== 'pinnedGroup') {
181
+ registerItem({
182
+ [id]: {
183
+ label,
184
+ active,
185
+ onClick
186
+ }
187
+ });
188
+ }
189
+ },
190
+ // eslint-disable-next-line react-hooks/exhaustive-deps
191
+ [active, id, label, registerItem]);
149
192
  const [internalToggle, setToggle] = useState(toggle !== false);
150
193
  const triggerToggle = useCallback(value => {
151
194
  setToggle(value);
@@ -161,21 +204,20 @@ const Item = ({
161
204
  }, 1);
162
205
  }
163
206
  }, [animation, toggle]);
164
- const PaddedStack = noExpand ? Stack : PaddingStack;
207
+ const PaddedStack = noExpand || type === 'pinnedGroup' ? Stack : PaddingStack;
165
208
  const hasHrefAndNoChildren = href && !children;
166
209
  const hasPinnedFeatureAndNoChildren = pinnedFeature && !children && !noPinButton;
167
- const isItemPinned = pinnedItems.includes(label);
210
+ const isItemPinned = pinnedItems.includes(id);
168
211
  const shouldShowPinnedButton = useMemo(() => {
169
212
  if (href || disabled) return false;
170
- if (pinnedItems.length >= pinLimit && type === 'default') return false;
171
213
  if (hasPinnedFeatureAndNoChildren && type !== 'default') {
172
214
  return true;
173
215
  }
174
- if (hasPinnedFeatureAndNoChildren && !isItemPinned) {
216
+ if (hasPinnedFeatureAndNoChildren) {
175
217
  return true;
176
218
  }
177
219
  return false;
178
- }, [disabled, hasPinnedFeatureAndNoChildren, href, isItemPinned, pinLimit, pinnedItems.length, type]);
220
+ }, [disabled, hasPinnedFeatureAndNoChildren, href, type]);
179
221
  const hasActiveChildren = useMemo(() => {
180
222
  if (!children) return false;
181
223
  return Children.map(children, child => /*#__PURE__*/isValidElement(child) ? child.props?.active : false).includes(true);
@@ -193,7 +235,7 @@ const Item = ({
193
235
  return 'button';
194
236
  }, [as, hasHrefAndNoChildren, noExpand]);
195
237
  const Container = StyledContainer.withComponent(containerTag, {
196
- target: "e134hokc12"
238
+ target: "e134hokc13"
197
239
  });
198
240
  const ariaExpanded = useMemo(() => {
199
241
  if (hasHrefAndNoChildren && internalToggle) {
@@ -204,6 +246,28 @@ const Item = ({
204
246
  }
205
247
  return undefined;
206
248
  }, [hasHrefAndNoChildren, internalToggle]);
249
+ const isPinDisabled = pinnedItems.length >= pinLimit;
250
+ const pinTooltipLocale = useMemo(() => {
251
+ if (isPinDisabled) {
252
+ return locales['navigation.pin.limit'];
253
+ }
254
+ if (isItemPinned) {
255
+ return locales['navigation.unpin.tooltip'];
256
+ }
257
+ return locales['navigation.pin.tooltip'];
258
+ }, [isItemPinned, isPinDisabled, locales]);
259
+ const onDragStartTrigger = event => {
260
+ event.dataTransfer.setData('text/plain', JSON.stringify({
261
+ label,
262
+ index
263
+ }));
264
+ // eslint-disable-next-line no-param-reassign
265
+ event.currentTarget.style.opacity = '0.5';
266
+ };
267
+ const onDragStopTrigger = event => {
268
+ // eslint-disable-next-line no-param-reassign
269
+ event.currentTarget.style.opacity = '1';
270
+ };
207
271
 
208
272
  // This content is when the navigation is expanded
209
273
  if (expanded || !expanded && animation === 'expand') {
@@ -233,6 +297,10 @@ const Item = ({
233
297
  "data-has-active-children": hasActiveChildren,
234
298
  "data-has-no-expand": noExpand,
235
299
  disabled: disabled,
300
+ draggable: type === 'pinned' && expanded,
301
+ onDragStart: event => expanded ? onDragStartTrigger(event) : undefined,
302
+ onDragEnd: event => expanded ? onDragStopTrigger(event) : undefined,
303
+ id: id,
236
304
  children: [jsxs(Stack, {
237
305
  direction: "row",
238
306
  gap: 1,
@@ -246,6 +314,12 @@ const Item = ({
246
314
  variant: active ? 'primary' : categoryIconVariant,
247
315
  disabled: disabled
248
316
  })
317
+ }) : null, type === 'pinned' && expanded ? jsx(GrabIcon, {
318
+ name: "drag-vertical",
319
+ sentiment: "neutral",
320
+ prominence: "weak",
321
+ size: "small",
322
+ disabled: disabled
249
323
  }) : null, jsxs(Stack, {
250
324
  children: [jsx(WrapText, {
251
325
  as: "span",
@@ -278,7 +352,7 @@ const Item = ({
278
352
  disabled: disabled,
279
353
  children: badgeText
280
354
  }) : null, shouldShowPinnedButton ? jsx(Tooltip, {
281
- text: isItemPinned ? locales['navigation.unpin.tooltip'] : locales['navigation.pin.tooltip'],
355
+ text: isItemPinned ? locales['navigation.unpin.tooltip'] : pinTooltipLocale,
282
356
  placement: "right",
283
357
  children: jsx("div", {
284
358
  style: {
@@ -288,8 +362,17 @@ const Item = ({
288
362
  size: "xsmall",
289
363
  variant: "ghost",
290
364
  sentiment: active ? 'primary' : 'neutral',
291
- onClick: () => isItemPinned ? unpinItem(label) : pinItem(label),
292
- icon: "pin"
365
+ onClick: event => {
366
+ if (isItemPinned) {
367
+ unpinItem(id);
368
+ } else {
369
+ pinItem(id);
370
+ }
371
+ event.stopPropagation(); // This is to avoid click spread to the parent and change the routing
372
+ },
373
+ icon: isItemPinned ? 'unpin' : 'pin',
374
+ iconVariant: isItemPinned ? 'filled' : 'outlined',
375
+ disabled: isItemPinned ? false : isPinDisabled
293
376
  })
294
377
  })
295
378
  }) : null]
@@ -298,21 +381,15 @@ const Item = ({
298
381
  sentiment: "neutral",
299
382
  prominence: "default",
300
383
  disabled: disabled
301
- }) : null, children ? jsxs(Stack, {
384
+ }) : null, children ? jsx(Stack, {
302
385
  gap: 1,
303
386
  direction: "row",
304
387
  alignItems: "center",
305
- children: [type === 'pinnedGroup' && pinLimit !== Infinity ? jsxs(WrapText, {
306
- as: "span",
307
- variant: "caption",
308
- sentiment: "neutral",
309
- prominence: "weak",
310
- children: [pinnedItems.length, "/", pinLimit]
311
- }) : null, !animation && !noExpand ? jsx(AnimatedIcon, {
388
+ children: !animation && !noExpand ? jsx(AnimatedIcon, {
312
389
  name: internalToggle ? 'arrow-down' : 'arrow-right',
313
390
  sentiment: "neutral",
314
391
  prominence: "weak"
315
- }) : null]
392
+ }) : null
316
393
  }) : null]
317
394
  })]
318
395
  }), children ? jsx(Fragment, {
@@ -336,7 +413,9 @@ const Item = ({
336
413
  alignItems: "start",
337
414
  justifyContent: "start",
338
415
  children: Children.count(children) > 0 ? jsx(StyledMenu, {
339
- triggerMethod: "hover",
416
+ triggerMethod: "click",
417
+ dynamicDomRendering: false // As we parse the children we don't need dynamic rendering
418
+ ,
340
419
  disclosure: jsx(Button, {
341
420
  sentiment: "neutral",
342
421
  variant: hasActiveChildren ? 'filled' : 'ghost',
@@ -354,11 +433,8 @@ const Item = ({
354
433
  }) : null
355
434
  }),
356
435
  placement: "right",
357
- children: ({
358
- toggle: toggleMenuLocal
359
- }) => Children.map(children, child => /*#__PURE__*/isValidElement(child) ? /*#__PURE__*/cloneElement(child, {
360
- hasParents: true,
361
- toggleMenu: toggleMenuLocal
436
+ children: Children.map(children, child => /*#__PURE__*/isValidElement(child) ? /*#__PURE__*/cloneElement(child, {
437
+ hasParents: true
362
438
  }) : child)
363
439
  }) : jsx(Tooltip, {
364
440
  text: label,
@@ -390,17 +466,19 @@ const Item = ({
390
466
  href: href,
391
467
  onClick: () => {
392
468
  onClick?.();
393
- toggleMenu?.();
394
469
  },
395
470
  borderless: true,
396
471
  active: active,
472
+ disabled: disabled,
397
473
  sentiment: active ? 'primary' : 'neutral',
474
+ isPinnable: shouldShowPinnedButton,
398
475
  children: jsxs(Stack, {
399
476
  gap: 1,
400
477
  direction: "row",
401
478
  alignItems: "center",
402
479
  justifyContent: "space-between",
403
480
  flex: 1,
481
+ width: "100%",
404
482
  children: [jsx(WrapText, {
405
483
  as: "span",
406
484
  variant: "bodySmall",
@@ -417,20 +495,28 @@ const Item = ({
417
495
  prominence: "weak",
418
496
  disabled: disabled
419
497
  }) : null, shouldShowPinnedButton ? jsx(Tooltip, {
420
- text: isItemPinned ? locales['navigation.unpin.tooltip'] : locales['navigation.pin.tooltip'],
498
+ text: isItemPinned ? locales['navigation.unpin.tooltip'] : pinTooltipLocale,
421
499
  placement: "right",
422
- children: jsx(CollapsedPinnedButton, {
423
- size: "xsmall",
424
- variant: "ghost",
425
- sentiment: active ? 'primary' : 'neutral',
426
- onClick: () => {
427
- if (isItemPinned) {
428
- unpinItem(label);
429
- } else {
430
- pinItem(label);
431
- }
500
+ children: jsx("div", {
501
+ style: {
502
+ position: 'relative'
432
503
  },
433
- icon: "auto-fix"
504
+ children: jsx(CollapsedPinnedButton, {
505
+ size: "xsmall",
506
+ variant: "ghost",
507
+ sentiment: active ? 'primary' : 'neutral',
508
+ onClick: event => {
509
+ if (isItemPinned) {
510
+ unpinItem(id);
511
+ } else {
512
+ pinItem(id);
513
+ }
514
+ event.stopPropagation(); // This is to avoid click spread to the parent and change the routing
515
+ },
516
+ icon: isItemPinned ? 'unpin' : 'pin',
517
+ iconVariant: isItemPinned ? 'filled' : 'outlined',
518
+ disabled: isItemPinned ? false : isPinDisabled
519
+ })
434
520
  })
435
521
  }) : null]
436
522
  })
@@ -1,8 +1,41 @@
1
+ import _styled from '@emotion/styled/base';
2
+ import { useTheme } from '@emotion/react';
3
+ import { Text } from '@ultraviolet/ui';
4
+ import { useCallback } from 'react';
1
5
  import { useNavigation } from '../NavigationProvider.js';
2
6
  import { Item } from './Item.js';
3
- import { jsx } from '@emotion/react/jsx-runtime';
7
+ import { jsx, jsxs } from '@emotion/react/jsx-runtime';
4
8
 
5
- const PinnedItems = () => {
9
+ function _EMOTION_STRINGIFIED_CSS_ERROR__() { return "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."; }
10
+ const DropableArea = /*#__PURE__*/_styled("div", {
11
+ target: "e5ys0ny2"
12
+ })("position:absolute;right:0;left:0;top:0;height:2px;border-top:2px solid;border-color:transparent;padding:", ({
13
+ theme
14
+ }) => theme.space['0.5'], " 0;&::before{content:'';position:absolute;left:0;top:-4px;height:0px;width:0px;border:3px solid;border-color:inherit;border-radius:", ({
15
+ theme
16
+ }) => theme.radii.circle, ";}");
17
+ const RelativeDiv = /*#__PURE__*/_styled("div", {
18
+ target: "e5ys0ny1"
19
+ })(process.env.NODE_ENV === "production" ? {
20
+ name: "bjn8wh",
21
+ styles: "position:relative"
22
+ } : {
23
+ name: "bjn8wh",
24
+ styles: "position:relative",
25
+ toString: _EMOTION_STRINGIFIED_CSS_ERROR__
26
+ });
27
+ const TextContainer = /*#__PURE__*/_styled("div", {
28
+ target: "e5ys0ny0"
29
+ })("padding:", ({
30
+ theme
31
+ }) => theme.space['1'], " 0;padding-left:", ({
32
+ theme
33
+ }) => theme.space['4'], ";margin-left:", ({
34
+ theme
35
+ }) => theme.space['0.5'], ";");
36
+ const PinnedItems = ({
37
+ toggle = true
38
+ }) => {
6
39
  const context = useNavigation();
7
40
  if (!context) {
8
41
  throw new Error('Navigation.PinnedItems can only be used inside a NavigationProvider.');
@@ -10,18 +43,84 @@ const PinnedItems = () => {
10
43
  const {
11
44
  locales,
12
45
  pinnedItems,
13
- pinnedFeature
46
+ pinnedFeature,
47
+ reorderItems,
48
+ expanded,
49
+ items
14
50
  } = context;
15
- if (pinnedItems.length > 0 && pinnedFeature) {
16
- return jsx(Item, {
17
- label: locales['navigation.pinned.item.group.label'],
18
- categoryIcon: "pin",
19
- toggle: false,
20
- type: "pinnedGroup",
21
- children: pinnedItems.map(item => jsx(Item, {
22
- label: item,
23
- type: "pinned"
24
- }, item))
51
+ const theme = useTheme();
52
+ const onDrop = useCallback((event, index) => {
53
+ event.preventDefault();
54
+ if (event?.dataTransfer) {
55
+ // eslint-disable-next-line no-param-reassign
56
+ event.currentTarget.style.borderColor = 'transparent';
57
+ const data = JSON.parse(event.dataTransfer.getData('text'));
58
+ if (data.index === index - 1 || index > data.index) {
59
+ reorderItems(data.index, index - 1);
60
+ return;
61
+ }
62
+ reorderItems(data.index, index);
63
+ }
64
+ }, [reorderItems]);
65
+ const onDragOver = useCallback(event => {
66
+ event.preventDefault();
67
+ // eslint-disable-next-line no-param-reassign
68
+ event.currentTarget.style.borderColor = theme.colors.primary.border;
69
+ }, [theme.colors.primary.border]);
70
+ const onDragLeave = useCallback(event => {
71
+ event.preventDefault();
72
+ // eslint-disable-next-line no-param-reassign
73
+ event.currentTarget.style.borderColor = 'transparent';
74
+ }, []);
75
+ if (Object.keys(items).length === 0) {
76
+ return null;
77
+ }
78
+ if (pinnedFeature) {
79
+ return jsx("div", {
80
+ children: jsxs(Item, {
81
+ label: locales['navigation.pinned.item.group.label'],
82
+ categoryIcon: "pin",
83
+ categoryIconVariant: "neutral",
84
+ toggle: toggle,
85
+ type: "pinnedGroup",
86
+ id: "pinned-group",
87
+ children: [pinnedItems.length > 0 ? pinnedItems.map((itemId, index) => jsxs("div", {
88
+ style: {
89
+ position: expanded ? 'relative' : undefined
90
+ },
91
+ children: [expanded ? jsx(DropableArea, {
92
+ onDragOver: onDragOver,
93
+ onDragLeave: onDragLeave,
94
+ onDrop: event => onDrop(event, index)
95
+ }) : null, jsx(Item, {
96
+ label: items[itemId]?.label ?? 'Unknown navigation item',
97
+ type: "pinned",
98
+ index: index,
99
+ toggle: toggle,
100
+ id: itemId,
101
+ active: items[itemId]?.active ?? false,
102
+ onClick: items[itemId]?.onClick ?? undefined,
103
+ hasParents: true
104
+ })]
105
+ }, itemId)) : jsx(TextContainer, {
106
+ children: jsx(Text, {
107
+ as: "p",
108
+ variant: "caption",
109
+ prominence: "weak",
110
+ sentiment: "neutral",
111
+ children: locales['navigation.pinned.item.group.empty']
112
+ })
113
+ }), expanded ? jsx(RelativeDiv, {
114
+ style: {
115
+ position: 'relative'
116
+ },
117
+ children: jsx(DropableArea, {
118
+ onDragOver: onDragOver,
119
+ onDragLeave: onDragLeave,
120
+ onDrop: event => onDrop(event, pinnedItems.length)
121
+ })
122
+ }) : null]
123
+ })
25
124
  });
26
125
  }
27
126
  return null;
@@ -8,7 +8,6 @@ const NAVIGATION_MAX_WIDTH = 320; // in px
8
8
  /**
9
9
  * ANIMATIONS
10
10
  */
11
- const PIN_BUTTON_OPACITY_TRANSITION = 'opacity 250ms ease-in-out'; // this is the transition animation when hovering pin button
12
11
  const ANIMATION_DURATION = 700; // collapse and expand animation duration of the whole nav in ms
13
12
 
14
13
  const shrinkHeight = keyframes`
@@ -34,4 +33,4 @@ const groupAnimation = keyframes`
34
33
  }
35
34
  `;
36
35
 
37
- export { ANIMATION_DURATION, NAVIGATION_COLLASPED_WIDTH, NAVIGATION_MAX_WIDTH, NAVIGATION_MIN_WIDTH, NAVIGATION_WIDTH, PIN_BUTTON_OPACITY_TRANSITION, groupAnimation, shrinkHeight };
36
+ export { ANIMATION_DURATION, NAVIGATION_COLLASPED_WIDTH, NAVIGATION_MAX_WIDTH, NAVIGATION_MIN_WIDTH, NAVIGATION_WIDTH, groupAnimation, shrinkHeight };
@@ -1,9 +1,11 @@
1
1
  var NavigationLocales = {
2
2
  'navigation.pin.tooltip': 'Pin product',
3
3
  'navigation.unpin.tooltip': 'Unpin product',
4
+ 'navigation.pin.limit': 'You cannot pin more products',
4
5
  'navigation.pinned.item.group.label': 'Pinned items',
5
6
  'navigation.expand.button': 'Expand sidebar',
6
- 'navigation.collapse.button': 'Collapse sidebar'
7
+ 'navigation.collapse.button': 'Collapse sidebar',
8
+ 'navigation.pinned.item.group.empty': 'You have no pinned items.'
7
9
  };
8
10
 
9
11
  export { NavigationLocales as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultraviolet/plus",
3
- "version": "0.14.3",
3
+ "version": "0.15.0",
4
4
  "description": "Ultraviolet Plus",
5
5
  "homepage": "https://github.com/scaleway/ultraviolet#readme",
6
6
  "repository": {
@@ -35,18 +35,18 @@
35
35
  "peerDependencies": {
36
36
  "@emotion/react": "11.11.4",
37
37
  "@emotion/styled": "11.11.5",
38
- "react": "18.2.0",
39
- "react-dom": "18.2.0"
38
+ "react": "18.3.1",
39
+ "react-dom": "18.3.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@babel/core": "7.24.4",
42
+ "@babel/core": "7.24.5",
43
43
  "@emotion/babel-plugin": "11.11.0",
44
44
  "@emotion/react": "11.11.4",
45
45
  "@emotion/styled": "11.11.5",
46
- "@types/react": "18.2.79",
47
- "@types/react-dom": "18.2.25",
48
- "react": "18.2.0",
49
- "react-dom": "18.2.0",
46
+ "@types/react": "18.3.1",
47
+ "@types/react-dom": "18.3.0",
48
+ "react": "18.3.1",
49
+ "react-dom": "18.3.1",
50
50
  "@ultraviolet/icons": "2.12.5",
51
51
  "@ultraviolet/illustrations": "1.7.1"
52
52
  },
@@ -56,7 +56,7 @@
56
56
  "@uiw/react-codemirror": "4.21.25",
57
57
  "react-intersection-observer": "9.8.2",
58
58
  "@ultraviolet/themes": "1.10.0",
59
- "@ultraviolet/ui": "1.50.0"
59
+ "@ultraviolet/ui": "1.51.1"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "rollup -c ../../rollup.config.mjs",