@developer_tribe/react-builder 0.1.32 → 1.0.1

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.
Files changed (86) hide show
  1. package/dist/DeviceMockFrame.d.ts +1 -17
  2. package/dist/RenderPage.d.ts +1 -9
  3. package/dist/build-components/index.d.ts +1 -0
  4. package/dist/components/AttributesEditorPanel.d.ts +9 -0
  5. package/dist/components/Breadcrumb.d.ts +13 -0
  6. package/dist/components/Builder.d.ts +9 -0
  7. package/dist/components/EditorHeader.d.ts +15 -0
  8. package/dist/index.cjs.js +6 -5
  9. package/dist/index.cjs.js.map +1 -0
  10. package/dist/index.d.ts +8 -4
  11. package/dist/index.esm.js +6 -5
  12. package/dist/index.esm.js.map +1 -0
  13. package/dist/pages/ProjectPage.d.ts +11 -0
  14. package/dist/pages/tabs/BuilderTab.d.ts +9 -0
  15. package/dist/pages/tabs/DebugTab.d.ts +7 -0
  16. package/dist/pages/tabs/PreviewTab.d.ts +3 -0
  17. package/dist/store.d.ts +17 -18
  18. package/dist/styles.css +1 -1
  19. package/dist/types/PreviewConfig.d.ts +6 -3
  20. package/dist/types/Project.d.ts +12 -2
  21. package/dist/utils/copyNode.d.ts +2 -0
  22. package/dist/utils/logger.d.ts +11 -0
  23. package/dist/utils/useLogRender.d.ts +1 -0
  24. package/package.json +16 -9
  25. package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
  26. package/src/AttributesEditor.tsx +2 -0
  27. package/src/DeviceMockFrame.tsx +22 -31
  28. package/src/RenderPage.tsx +5 -42
  29. package/src/assets/images/android.svg +43 -0
  30. package/src/assets/images/apple.svg +16 -0
  31. package/src/assets/images/background.jpg +0 -0
  32. package/src/assets/samples/carousel-sample.json +2 -3
  33. package/src/assets/samples/getSamples.ts +49 -12
  34. package/src/assets/samples/simple-1.json +1 -2
  35. package/src/assets/samples/simple-2.json +1 -2
  36. package/src/assets/samples/vpn-onboard-1.json +1 -2
  37. package/src/assets/samples/vpn-onboard-2.json +1 -2
  38. package/src/assets/samples/vpn-onboard-3.json +1 -2
  39. package/src/assets/samples/vpn-onboard-4.json +1 -2
  40. package/src/assets/samples/vpn-onboard-5.json +1 -2
  41. package/src/assets/samples/vpn-onboard-6.json +1 -2
  42. package/src/build-components/Button/Button.tsx +2 -0
  43. package/src/build-components/Carousel/Carousel.tsx +2 -0
  44. package/src/build-components/CarouselButtons/CarouselButtons.tsx +2 -0
  45. package/src/build-components/CarouselDots/CarouselDots.tsx +2 -0
  46. package/src/build-components/CarouselItem/CarouselItem.tsx +2 -0
  47. package/src/build-components/Image/Image.tsx +2 -0
  48. package/src/build-components/Onboard/Onboard.tsx +2 -0
  49. package/src/build-components/OnboardButton/OnboardButton.tsx +7 -4
  50. package/src/build-components/OnboardButtons/OnboardButtons.tsx +7 -7
  51. package/src/build-components/OnboardDot/OnboardDot.tsx +2 -0
  52. package/src/build-components/OnboardFooter/OnboardFooter.tsx +5 -3
  53. package/src/build-components/OnboardImage/OnboardImage.tsx +2 -0
  54. package/src/build-components/OnboardItem/OnboardItem.tsx +2 -0
  55. package/src/build-components/OnboardProvider/OnboardProvider.tsx +2 -0
  56. package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +2 -0
  57. package/src/build-components/OnboardTitle/OnboardTitle.tsx +2 -0
  58. package/src/build-components/Text/Text.tsx +5 -3
  59. package/src/build-components/View/View.tsx +2 -0
  60. package/src/build-components/index.ts +22 -0
  61. package/src/components/AttributesEditorPanel.tsx +112 -0
  62. package/src/components/Breadcrumb.tsx +48 -0
  63. package/src/components/Builder.tsx +272 -0
  64. package/src/components/EditorHeader.tsx +186 -0
  65. package/src/index.ts +8 -4
  66. package/src/pages/ProjectPage.tsx +152 -0
  67. package/src/pages/tabs/BuilderTab.tsx +33 -0
  68. package/src/pages/tabs/DebugTab.tsx +23 -0
  69. package/src/pages/tabs/PreviewTab.tsx +194 -0
  70. package/src/size-matters/index.ts +5 -1
  71. package/src/store.ts +60 -38
  72. package/src/styles/_mixins.scss +21 -0
  73. package/src/styles/_variables.scss +27 -0
  74. package/src/styles/builder.scss +60 -0
  75. package/src/styles/components.scss +88 -0
  76. package/src/styles/editor.scss +174 -0
  77. package/src/styles/global.scss +200 -0
  78. package/src/styles/index.scss +7 -0
  79. package/src/styles/pages.scss +2 -0
  80. package/src/types/PreviewConfig.ts +14 -5
  81. package/src/types/Project.ts +15 -2
  82. package/src/utils/copyNode.ts +7 -0
  83. package/src/utils/extractTextStyle.ts +4 -2
  84. package/src/utils/getDevices.ts +1 -0
  85. package/src/utils/logger.ts +76 -0
  86. package/src/utils/useLogRender.ts +13 -0
@@ -2,9 +2,8 @@
2
2
  "name": "vpn-onboard-4 (legacy)",
3
3
  "version": "0.0.0",
4
4
  "type": "nova",
5
- "previewConfig": {
5
+ "appConfig": {
6
6
  "theme": "dark",
7
- "screenSize": { "width": 375, "height": 812 },
8
7
  "isRtl": false,
9
8
  "screenStyle": {
10
9
  "light": { "backgroundColor": "#FDFDFD", "color": "#161827" },
@@ -2,9 +2,8 @@
2
2
  "name": "vpn-onboard-5 (legacy)",
3
3
  "version": "0.0.0",
4
4
  "type": "nova",
5
- "previewConfig": {
5
+ "appConfig": {
6
6
  "theme": "light",
7
- "screenSize": { "width": 375, "height": 812 },
8
7
  "isRtl": false,
9
8
  "screenStyle": {
10
9
  "light": { "backgroundColor": "#FDFDFD", "color": "#161827" },
@@ -2,9 +2,8 @@
2
2
  "name": "vpn-onboard-6 (legacy)",
3
3
  "version": "0.0.0",
4
4
  "type": "nova",
5
- "previewConfig": {
5
+ "appConfig": {
6
6
  "theme": "dark",
7
- "screenSize": { "width": 375, "height": 812 },
8
7
  "isRtl": false,
9
8
  "screenStyle": {
10
9
  "light": { "backgroundColor": "#FDFDFD", "color": "#161827" },
@@ -1,8 +1,10 @@
1
1
  import React from 'react';
2
2
  import type { ButtonComponentProps } from './ButtonProps.generated';
3
3
  import useNode from '../useNode';
4
+ import { useLogRender } from '../../utils/useLogRender';
4
5
 
5
6
  function Button({ node }: ButtonComponentProps) {
7
+ useLogRender('Button');
6
8
  node = useNode(node);
7
9
  return String(node?.type ?? 'button');
8
10
  }
@@ -3,8 +3,10 @@ import type { CarouselComponentProps } from './CarouselProps.generated';
3
3
  import RenderNode from '../RenderNode.generated';
4
4
  import { isCarouselItem } from '../../utils/isCarousel';
5
5
  import useNode from '../useNode';
6
+ import { useLogRender } from '../../utils/useLogRender';
6
7
 
7
8
  function Carousel({ node }: CarouselComponentProps) {
9
+ useLogRender('Carousel');
8
10
  node = useNode(node);
9
11
  // Ensure children are carouselItems
10
12
  const renderChildren = () => {
@@ -2,8 +2,10 @@ import React, { useContext } from 'react';
2
2
  import type { CarouselButtonsComponentProps } from './CarouselButtonsProps.generated';
3
3
  import { carouselContext } from '../CarouselProvider/CarouselProvider';
4
4
  import useNode from '../useNode';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  function CarouselButtons({ node }: CarouselButtonsComponentProps) {
8
+ useLogRender('CarouselButtons');
7
9
  node = useNode(node);
8
10
  const emblaApi = useContext(carouselContext);
9
11
  const buttonTypes = node.attributes?.buttonType || [
@@ -2,8 +2,10 @@ import React, { useContext, useEffect, useState } from 'react';
2
2
  import type { CarouselDotsComponentProps } from './CarouselDotsProps.generated';
3
3
  import { carouselContext } from '../CarouselProvider/CarouselProvider';
4
4
  import useNode from '../useNode';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  function CarouselDots({ node }: CarouselDotsComponentProps) {
8
+ useLogRender('CarouselDots');
7
9
  node = useNode(node);
8
10
  const dotType = node.attributes?.dotType || 'normal_dot';
9
11
  const emblaApi = useContext(carouselContext);
@@ -2,8 +2,10 @@ import React from 'react';
2
2
  import type { CarouselItemComponentProps } from './CarouselItemProps.generated';
3
3
  import { RenderNode } from '../..';
4
4
  import useNode from '../useNode';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  export function CarouselItem({ node }: CarouselItemComponentProps) {
8
+ useLogRender('CarouselItem');
7
9
  node = useNode(node);
8
10
  return (
9
11
  <div className="embla__slide" {...node.attributes}>
@@ -2,8 +2,10 @@ import React from 'react';
2
2
  import type { ImageComponentProps } from './ImageProps.generated';
3
3
  import useNode from '../useNode';
4
4
  import { extractImageStyle } from '../../utils/extractImageStyle';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  function Image({ node }: ImageComponentProps) {
8
+ useLogRender('Image');
7
9
  node = useNode(node);
8
10
  return (
9
11
  <img
@@ -2,8 +2,10 @@ import React from 'react';
2
2
  import type { OnboardComponentProps } from './OnboardProps.generated';
3
3
  import Carousel from '../Carousel/Carousel';
4
4
  import useNode from '../useNode';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  function Onboard({ node }: OnboardComponentProps) {
8
+ useLogRender('Onboard');
7
9
  node = useNode(node);
8
10
  return <Carousel node={{ ...node, type: 'carousel' } as any} />;
9
11
  }
@@ -3,18 +3,21 @@ import type { OnboardButtonComponentProps } from './OnboardButtonProps.generated
3
3
  import { onboardContext } from '../OnboardProvider/OnboardProvider';
4
4
  import useNode from '../useNode';
5
5
  import { useRenderStore } from '../../store';
6
+ import { useLogRender } from '../../utils/useLogRender';
6
7
 
7
8
  function OnboardButton({ node }: OnboardButtonComponentProps) {
9
+ useLogRender('OnboardButton');
8
10
  node = useNode(node);
9
11
  const { emblaApi } = useContext(onboardContext) ?? {};
10
- const { defaultLanguage, localication } = useRenderStore((s) => ({
11
- defaultLanguage: s.defaultLanguage,
12
- localication: s.localication,
12
+ const { appConfig } = useRenderStore((s) => ({
13
+ appConfig: s.appConfig,
13
14
  }));
14
15
 
15
16
  const labelRaw = node.attributes?.labelKey ?? '';
16
17
  const label =
17
- (localication?.[defaultLanguage ?? 'en']?.[labelRaw] as string) ?? labelRaw;
18
+ (appConfig.localication?.[appConfig.defaultLanguage ?? 'en']?.[
19
+ labelRaw
20
+ ] as string) ?? labelRaw;
18
21
 
19
22
  const flex = node.attributes?.flex ?? 1;
20
23
  const textColor = node.attributes?.button_text_color ?? '#FFFFFF';
@@ -5,19 +5,19 @@ import { onboardContext } from '../OnboardProvider/OnboardProvider';
5
5
  import RenderNode from '../RenderNode.generated';
6
6
  import useNode from '../useNode';
7
7
  import { useRenderStore } from '../../store';
8
+ import { useLogRender } from '../../utils/useLogRender';
8
9
 
9
10
  function OnboardButtons({ node }: OnboardButtonsComponentProps) {
11
+ useLogRender('OnboardButtons');
10
12
  node = useNode(node);
11
- const { screenStyle, theme } = useRenderStore((s) => ({
12
- screenStyle: s.screenStyle,
13
- theme: s.theme,
13
+ const { appConfig } = useRenderStore((s) => ({
14
+ appConfig: s.appConfig,
14
15
  }));
15
16
  const seperatorColorDefault =
16
- theme === 'light'
17
- ? screenStyle.light.seperatorColor
18
- : screenStyle.dark.seperatorColor;
17
+ appConfig.theme === 'light'
18
+ ? appConfig.screenStyle.light.seperatorColor
19
+ : appConfig.screenStyle.dark.seperatorColor;
19
20
  const ctx = useContext(onboardContext) ?? {};
20
- const emblaApi = ctx.emblaApi;
21
21
  const [selectedIndex, setSelectedIndex] = useState(ctx.selectedIndex ?? 0);
22
22
 
23
23
  useEffect(() => {
@@ -2,8 +2,10 @@ import React from 'react';
2
2
  import type { OnboardDotComponentProps } from './OnboardExpandingDotProps.generated';
3
3
  import CarouselDots from '../CarouselDots/CarouselDots';
4
4
  import useNode from '../useNode';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  function OnboardDot({ node }: OnboardDotComponentProps) {
8
+ useLogRender('OnboardDot');
7
9
  node = useNode(node);
8
10
  return <CarouselDots node={{ ...node, type: 'carouselDots' } as any} />;
9
11
  }
@@ -4,6 +4,7 @@ import useNode from '../useNode';
4
4
  import { useRenderStore } from '../../store';
5
5
  import { parseSize } from '../../size-matters';
6
6
  import { extractTextStyle } from '../../utils/extractTextStyle';
7
+ import { useLogRender } from '../../utils/useLogRender';
7
8
 
8
9
  type Segment =
9
10
  | { type: 'text'; value: string }
@@ -86,11 +87,12 @@ function buildSegments(
86
87
  }
87
88
 
88
89
  function OnboardFooter({ node }: OnboardFooterComponentProps) {
90
+ useLogRender('OnboardFooter');
89
91
  node = useNode(node);
90
- const { defaultLanguage, localication } = useRenderStore((s) => ({
91
- defaultLanguage: s.defaultLanguage,
92
- localication: s.localication,
92
+ const { appConfig } = useRenderStore((s) => ({
93
+ appConfig: s.appConfig,
93
94
  }));
95
+ const { localication, defaultLanguage } = appConfig;
94
96
  const t = (key?: string) =>
95
97
  key ? (localication?.[defaultLanguage ?? 'en']?.[key] ?? key) : '';
96
98
 
@@ -3,8 +3,10 @@ import type { OnboardImageComponentProps } from './OnboardImageProps.generated';
3
3
  import Image from '../Image/Image';
4
4
  import useNode from '../useNode';
5
5
  import Lottie from 'lottie-react';
6
+ import { useLogRender } from '../../utils/useLogRender';
6
7
 
7
8
  function OnboardImage({ node }: OnboardImageComponentProps) {
9
+ useLogRender('OnboardImage');
8
10
  node = useNode(node);
9
11
  const [lottie, setLottie] = useState<string | null>(null);
10
12
 
@@ -3,8 +3,10 @@ import type { OnboardItemComponentProps } from './OnboardItemProps.generated';
3
3
  import useNode from '../useNode';
4
4
  import { RenderNode } from '../..';
5
5
  import { parseSize } from '../../size-matters';
6
+ import { useLogRender } from '../../utils/useLogRender';
6
7
 
7
8
  function OnboardItem({ node }: OnboardItemComponentProps) {
9
+ useLogRender('OnboardItem');
8
10
  node = useNode(node);
9
11
  const flexDirection = node.attributes?.flexDirection;
10
12
  const display = node.attributes?.display;
@@ -11,9 +11,11 @@ import useEmblaCarousel from 'embla-carousel-react';
11
11
  import RenderNode from '../RenderNode.generated';
12
12
  import { useRenderStore } from '../../store';
13
13
  import useNode from '../useNode';
14
+ import { useLogRender } from '../../utils/useLogRender';
14
15
 
15
16
  export const onboardContext = createContext<any>(undefined);
16
17
  function OnboardProvider({ node }: OnboardProviderComponentProps) {
18
+ useLogRender('OnboardProvider');
17
19
  node = useNode(node);
18
20
  const device = useRenderStore((s) => s.device);
19
21
  const [emblaRef, emblaApi] = useEmblaCarousel(node.attributes as any);
@@ -2,8 +2,10 @@ import React from 'react';
2
2
  import type { OnboardSubtitleComponentProps } from './OnboardSubtitleProps.generated';
3
3
  import Text from '../Text/Text';
4
4
  import useNode from '../useNode';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  function OnboardSubtitle({ node }: OnboardSubtitleComponentProps) {
8
+ useLogRender('OnboardSubtitle');
7
9
  node = useNode(node);
8
10
  return <Text node={node} />;
9
11
  }
@@ -2,8 +2,10 @@ import React from 'react';
2
2
  import type { OnboardTitleComponentProps } from './OnboardTitleProps.generated';
3
3
  import Text from '../Text/Text';
4
4
  import useNode from '../useNode';
5
+ import { useLogRender } from '../../utils/useLogRender';
5
6
 
6
7
  function OnboardTitle({ node }: OnboardTitleComponentProps) {
8
+ useLogRender('OnboardTitle');
7
9
  node = useNode(node);
8
10
  return <Text node={node} />;
9
11
  }
@@ -3,13 +3,15 @@ import type { TextComponentProps } from './TextProps.generated';
3
3
  import useNode from '../useNode';
4
4
  import { useRenderStore } from '../../store';
5
5
  import { extractTextStyle } from '../../utils/extractTextStyle';
6
+ import { useLogRender } from '../../utils/useLogRender';
6
7
 
7
8
  function Text({ node }: TextComponentProps) {
9
+ useLogRender('Text');
8
10
  node = useNode(node);
9
- const { defaultLanguage, localication } = useRenderStore((s) => ({
10
- defaultLanguage: s.defaultLanguage,
11
- localication: s.localication,
11
+ const { appConfig } = useRenderStore((s) => ({
12
+ appConfig: s.appConfig,
12
13
  }));
14
+ const { defaultLanguage, localication } = appConfig;
13
15
  const keyOrText: string = node.children as string;
14
16
  const style = extractTextStyle(node);
15
17
 
@@ -7,8 +7,10 @@ import RenderNode from '../RenderNode.generated';
7
7
  import { Node } from '../../types/Node';
8
8
  import useNode from '../useNode';
9
9
  import { extractViewStyle } from '../../utils/extractViewStyle';
10
+ import { useLogRender } from '../../utils/useLogRender';
10
11
 
11
12
  export function View({ node }: ViewComponentProps) {
13
+ useLogRender('View');
12
14
  node = useNode(node);
13
15
  return (
14
16
  <div style={extractViewStyle(node)} className="scroll-container">
@@ -4,6 +4,28 @@ export { default as RenderNode } from './RenderNode.generated';
4
4
 
5
5
  export { patterns } from './patterns.generated';
6
6
 
7
+ export const allcomponentNames = [
8
+ 'button',
9
+ 'carousel',
10
+ 'carouselButtons',
11
+ 'carouselDots',
12
+ 'carouselItem',
13
+ 'carouselProvider',
14
+ 'image',
15
+ 'Onboard',
16
+ 'OnboardButton',
17
+ 'OnboardButtons',
18
+ 'OnboardDot',
19
+ 'OnboardFooter',
20
+ 'OnboardImage',
21
+ 'OnboardItem',
22
+ 'OnboardProvider',
23
+ 'OnboardSubtitle',
24
+ 'OnboardTitle',
25
+ 'text',
26
+ 'view',
27
+ ] as const;
28
+
7
29
  export type {
8
30
  ButtonPropsGenerated,
9
31
  ButtonComponentProps,
@@ -0,0 +1,112 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { AttributesEditor, Node, NodeData } from '..';
3
+ import { useLogRender } from '../utils/useLogRender';
4
+
5
+ interface AttributesEditorPanelProps {
6
+ current: Node;
7
+ attributes: any;
8
+ onChange: (data: Node) => void;
9
+ setCurrent: (current: Node) => void;
10
+ }
11
+
12
+ export function AttributesEditorPanel({
13
+ current,
14
+ attributes,
15
+ onChange,
16
+ setCurrent,
17
+ }: AttributesEditorPanelProps) {
18
+ useLogRender('AttributesEditorPanel');
19
+ if (!current) return null;
20
+
21
+ function replaceNode(root: Node, target: Node, next: Node): Node {
22
+ if (root === target) return next;
23
+ if (root === null || root === undefined) return root;
24
+ if (typeof root === 'string') return root;
25
+ if (Array.isArray(root)) {
26
+ let changed = false;
27
+ const arr = root.map((item) => {
28
+ const r = replaceNode(item, target, next);
29
+ if (r !== item) changed = true;
30
+ return r;
31
+ });
32
+ return changed ? arr : root;
33
+ }
34
+ const data = root as any;
35
+ if ('children' in data) {
36
+ const prev = data.children;
37
+ const replaced = Array.isArray(prev)
38
+ ? prev.map((c: Node) => replaceNode(c, target, next))
39
+ : replaceNode(prev as Node, target, next);
40
+ if (replaced !== prev) {
41
+ data.children = replaced;
42
+ return { ...data, children: replaced } as Node;
43
+ }
44
+ }
45
+ return root;
46
+ }
47
+ const [draft, setDraft] = useState<Node>(current);
48
+ const [isAllowedTopAccept, setIsAllowedTopAccept] = useState<boolean>(false);
49
+
50
+ const isEqual = useMemo(() => {
51
+ try {
52
+ return JSON.stringify(current) === JSON.stringify(draft);
53
+ } catch {
54
+ return current === draft;
55
+ }
56
+ }, [current, draft]);
57
+
58
+ useEffect(() => {
59
+ setDraft(current);
60
+ setIsAllowedTopAccept(false);
61
+ }, [current, attributes]);
62
+
63
+ useEffect(() => {
64
+ setIsAllowedTopAccept(!isEqual);
65
+ }, [isEqual]);
66
+
67
+ return (
68
+ <div style={{ padding: 12 }}>
69
+ <h2>{(current as NodeData).type ?? current?.toString() ?? '?'}</h2>{' '}
70
+ <div
71
+ style={{
72
+ display: 'flex',
73
+ alignItems: 'center',
74
+ gap: 8,
75
+ padding: '16px 0',
76
+ }}
77
+ >
78
+ <h3 style={{ marginTop: 0, marginBottom: 0, flex: '1 1 auto' }}>
79
+ Attributes
80
+ </h3>
81
+ <button
82
+ disabled={!isAllowedTopAccept}
83
+ onClick={() => {
84
+ setDraft(current);
85
+ setIsAllowedTopAccept(false);
86
+ }}
87
+ >
88
+ Revert
89
+ </button>
90
+ <button
91
+ disabled={!isAllowedTopAccept}
92
+ onClick={() => {
93
+ const root = attributes as Node;
94
+ const updated = replaceNode(root, current, draft);
95
+ onChange(updated);
96
+ setIsAllowedTopAccept(false);
97
+ setCurrent(draft);
98
+ }}
99
+ >
100
+ Accept
101
+ </button>
102
+ </div>
103
+ <AttributesEditor
104
+ node={draft}
105
+ onChange={(next: Node) => {
106
+ setDraft(next);
107
+ setIsAllowedTopAccept(true);
108
+ }}
109
+ />
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,48 @@
1
+ import { ReactNode } from 'react';
2
+ import { useLogRender } from '../utils/useLogRender';
3
+ export type BreadcrumbItem = {
4
+ label: string;
5
+ to?: string;
6
+ onClick?: () => void;
7
+ };
8
+
9
+ type BreadcrumbProps = {
10
+ items: BreadcrumbItem[];
11
+ separator?: ReactNode;
12
+ ariaLabel?: string;
13
+ };
14
+
15
+ export function Breadcrumb({
16
+ items,
17
+ separator = '/',
18
+ ariaLabel = 'Breadcrumb',
19
+ }: BreadcrumbProps) {
20
+ useLogRender('Breadcrumb');
21
+ return (
22
+ <nav className="breadcrumb" aria-label={ariaLabel}>
23
+ <ol className="breadcrumb__list">
24
+ {items.map((item, index) => {
25
+ const isLast = index === items.length - 1;
26
+ return (
27
+ <li className="breadcrumb__item" key={`${item.label}-${index}`}>
28
+ {index > 0 && (
29
+ <span className="breadcrumb__separator" aria-hidden>
30
+ {separator}
31
+ </span>
32
+ )}
33
+ {isLast || !item.to ? (
34
+ <span className="breadcrumb__current" aria-current="page">
35
+ {item.label}
36
+ </span>
37
+ ) : (
38
+ <p onClick={item.onClick} className="breadcrumb__link">
39
+ {item.label}
40
+ </p>
41
+ )}
42
+ </li>
43
+ );
44
+ })}
45
+ </ol>
46
+ </nav>
47
+ );
48
+ }