@teambit/react.ui.component-highlighter 0.0.0-4d64e75088ad738b699f1df7db0bcd7201d589d0

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 (134) hide show
  1. package/children-highlighter/children-highlighter.composition.tsx +103 -0
  2. package/children-highlighter/children-highlighter.spec.tsx +22 -0
  3. package/children-highlighter/children-highlighter.tsx +9 -0
  4. package/children-highlighter/index.ts +5 -0
  5. package/children-highlighter/use-children-highlighter.tsx +79 -0
  6. package/component-highlighter.docs.md +191 -0
  7. package/dist/children-highlighter/children-highlighter.composition.d.ts +6 -0
  8. package/dist/children-highlighter/children-highlighter.composition.js +93 -0
  9. package/dist/children-highlighter/children-highlighter.composition.js.map +1 -0
  10. package/dist/children-highlighter/children-highlighter.d.ts +4 -0
  11. package/dist/children-highlighter/children-highlighter.js +24 -0
  12. package/dist/children-highlighter/children-highlighter.js.map +1 -0
  13. package/dist/children-highlighter/children-highlighter.spec.d.ts +1 -0
  14. package/dist/children-highlighter/children-highlighter.spec.js +22 -0
  15. package/dist/children-highlighter/children-highlighter.spec.js.map +1 -0
  16. package/dist/children-highlighter/index.d.ts +4 -0
  17. package/dist/children-highlighter/index.js +8 -0
  18. package/dist/children-highlighter/index.js.map +1 -0
  19. package/dist/children-highlighter/use-children-highlighter.d.ts +18 -0
  20. package/dist/children-highlighter/use-children-highlighter.js +51 -0
  21. package/dist/children-highlighter/use-children-highlighter.js.map +1 -0
  22. package/dist/component-highlighter.docs.md +191 -0
  23. package/dist/element-highlighter/element-highlighter.compositions.d.ts +14 -0
  24. package/dist/element-highlighter/element-highlighter.compositions.js +103 -0
  25. package/dist/element-highlighter/element-highlighter.compositions.js.map +1 -0
  26. package/dist/element-highlighter/element-highlighter.d.ts +22 -0
  27. package/dist/element-highlighter/element-highlighter.js +31 -0
  28. package/dist/element-highlighter/element-highlighter.js.map +1 -0
  29. package/dist/element-highlighter/element-highlighter.module.scss +10 -0
  30. package/dist/element-highlighter/index.d.ts +2 -0
  31. package/dist/element-highlighter/index.js +6 -0
  32. package/dist/element-highlighter/index.js.map +1 -0
  33. package/dist/frame/frame.d.ts +13 -0
  34. package/dist/frame/frame.js +126 -0
  35. package/dist/frame/frame.js.map +1 -0
  36. package/dist/frame/frame.module.scss +23 -0
  37. package/dist/frame/index.d.ts +2 -0
  38. package/dist/frame/index.js +6 -0
  39. package/dist/frame/index.js.map +1 -0
  40. package/dist/hover-highlighter/bubble-to-component.d.ts +24 -0
  41. package/dist/hover-highlighter/bubble-to-component.js +55 -0
  42. package/dist/hover-highlighter/bubble-to-component.js.map +1 -0
  43. package/dist/hover-highlighter/bubble-to-component.spec.d.ts +1 -0
  44. package/dist/hover-highlighter/bubble-to-component.spec.js +38 -0
  45. package/dist/hover-highlighter/bubble-to-component.spec.js.map +1 -0
  46. package/dist/hover-highlighter/hover-highlighter.compositions.d.ts +4 -0
  47. package/dist/hover-highlighter/hover-highlighter.compositions.js +73 -0
  48. package/dist/hover-highlighter/hover-highlighter.compositions.js.map +1 -0
  49. package/dist/hover-highlighter/hover-highlighter.d.ts +4 -0
  50. package/dist/hover-highlighter/hover-highlighter.js +24 -0
  51. package/dist/hover-highlighter/hover-highlighter.js.map +1 -0
  52. package/dist/hover-highlighter/hover-highlighter.spec.d.ts +1 -0
  53. package/dist/hover-highlighter/hover-highlighter.spec.js +95 -0
  54. package/dist/hover-highlighter/hover-highlighter.spec.js.map +1 -0
  55. package/dist/hover-highlighter/index.d.ts +4 -0
  56. package/dist/hover-highlighter/index.js +8 -0
  57. package/dist/hover-highlighter/index.js.map +1 -0
  58. package/dist/hover-highlighter/use-hover-highlighter.d.ts +25 -0
  59. package/dist/hover-highlighter/use-hover-highlighter.js +47 -0
  60. package/dist/hover-highlighter/use-hover-highlighter.js.map +1 -0
  61. package/dist/hybrid-highlighter/hybrid-highlighter.d.ts +36 -0
  62. package/dist/hybrid-highlighter/hybrid-highlighter.js +79 -0
  63. package/dist/hybrid-highlighter/hybrid-highlighter.js.map +1 -0
  64. package/dist/hybrid-highlighter/index.d.ts +2 -0
  65. package/dist/hybrid-highlighter/index.js +6 -0
  66. package/dist/hybrid-highlighter/index.js.map +1 -0
  67. package/dist/ignore-highlighter.d.ts +19 -0
  68. package/dist/ignore-highlighter.js +25 -0
  69. package/dist/ignore-highlighter.js.map +1 -0
  70. package/dist/index.d.ts +10 -0
  71. package/dist/index.js +18 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/label/component-strip.compositions.d.ts +2 -0
  74. package/dist/label/component-strip.compositions.js +17 -0
  75. package/dist/label/component-strip.compositions.js.map +1 -0
  76. package/dist/label/component-strip.d.ts +7 -0
  77. package/dist/label/component-strip.js +61 -0
  78. package/dist/label/component-strip.js.map +1 -0
  79. package/dist/label/component-strip.module.scss +68 -0
  80. package/dist/label/index.d.ts +4 -0
  81. package/dist/label/index.js +8 -0
  82. package/dist/label/index.js.map +1 -0
  83. package/dist/label/label-container.d.ts +12 -0
  84. package/dist/label/label-container.js +75 -0
  85. package/dist/label/label-container.js.map +1 -0
  86. package/dist/label/label.d.ts +6 -0
  87. package/dist/label/label.js +60 -0
  88. package/dist/label/label.js.map +1 -0
  89. package/dist/label/label.module.scss +32 -0
  90. package/dist/label/links.d.ts +2 -0
  91. package/dist/label/links.js +16 -0
  92. package/dist/label/links.js.map +1 -0
  93. package/dist/label/other-components.d.ts +9 -0
  94. package/dist/label/other-components.js +34 -0
  95. package/dist/label/other-components.js.map +1 -0
  96. package/dist/mock-component.d.ts +14 -0
  97. package/dist/mock-component.js +43 -0
  98. package/dist/mock-component.js.map +1 -0
  99. package/dist/preview-1751963029152.js +10 -0
  100. package/dist/rule-matcher.d.ts +8 -0
  101. package/dist/rule-matcher.js +32 -0
  102. package/dist/rule-matcher.js.map +1 -0
  103. package/element-highlighter/element-highlighter.compositions.tsx +130 -0
  104. package/element-highlighter/element-highlighter.module.scss +10 -0
  105. package/element-highlighter/element-highlighter.tsx +51 -0
  106. package/element-highlighter/index.ts +2 -0
  107. package/frame/frame.module.scss +23 -0
  108. package/frame/frame.tsx +139 -0
  109. package/frame/index.ts +2 -0
  110. package/hover-highlighter/bubble-to-component.spec.tsx +57 -0
  111. package/hover-highlighter/bubble-to-component.tsx +82 -0
  112. package/hover-highlighter/hover-highlighter.compositions.tsx +65 -0
  113. package/hover-highlighter/hover-highlighter.spec.tsx +115 -0
  114. package/hover-highlighter/hover-highlighter.tsx +8 -0
  115. package/hover-highlighter/index.ts +5 -0
  116. package/hover-highlighter/use-hover-highlighter.tsx +85 -0
  117. package/hybrid-highlighter/hybrid-highlighter.tsx +136 -0
  118. package/hybrid-highlighter/index.ts +2 -0
  119. package/ignore-highlighter.tsx +22 -0
  120. package/index.ts +21 -0
  121. package/label/component-strip.compositions.tsx +13 -0
  122. package/label/component-strip.module.scss +68 -0
  123. package/label/component-strip.tsx +57 -0
  124. package/label/index.ts +5 -0
  125. package/label/label-container.tsx +72 -0
  126. package/label/label.module.scss +32 -0
  127. package/label/label.tsx +37 -0
  128. package/label/links.tsx +9 -0
  129. package/label/other-components.tsx +51 -0
  130. package/mock-component.tsx +23 -0
  131. package/package.json +61 -0
  132. package/rule-matcher.tsx +42 -0
  133. package/types/asset.d.ts +29 -0
  134. package/types/style.d.ts +42 -0
@@ -0,0 +1,115 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { act } from 'react-dom/test-utils';
3
+ import { render, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
4
+ import { ComponentMeta, componentMetaField } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
5
+
6
+ import { HoverHighlighter } from './hover-highlighter';
7
+
8
+ const debounceTime = 2;
9
+
10
+ function ButtonComponent({ children }: { children: ReactNode }) {
11
+ return <button>{children}</button>;
12
+ }
13
+
14
+ ButtonComponent[componentMetaField] = {
15
+ // could use a non-bit-id to render the "default" bubble
16
+ id: 'teambit.base-ui/input/button',
17
+ } as ComponentMeta;
18
+
19
+ it('should show bubble when hovering on element with bit id', async () => {
20
+ const { getByText, findByText } = render(
21
+ <HoverHighlighter debounceSelection={debounceTime}>
22
+ <ButtonComponent>hover here</ButtonComponent>
23
+ </HoverHighlighter>
24
+ );
25
+ const rendered = getByText('hover here');
26
+ expect(rendered).toBeTruthy();
27
+
28
+ act(() => {
29
+ fireEvent.mouseOver(rendered);
30
+ });
31
+
32
+ const highlightBubble = await findByText('input/button');
33
+ expect(highlightBubble).toBeInstanceOf(HTMLElement);
34
+ });
35
+
36
+ it('should hide the highlight when hovering out of the element', async () => {
37
+ const { getByText, findByText, queryByText } = render(
38
+ <HoverHighlighter debounceSelection={debounceTime}>
39
+ <ButtonComponent>hover here</ButtonComponent>
40
+ </HoverHighlighter>
41
+ );
42
+ const rendered = getByText('hover here');
43
+
44
+ act(() => {
45
+ fireEvent.mouseOver(rendered);
46
+ });
47
+
48
+ const highlightBubble = await findByText('input/button');
49
+ expect(highlightBubble).toBeInstanceOf(HTMLElement);
50
+
51
+ act(() => {
52
+ fireEvent.mouseOut(rendered);
53
+ });
54
+ await waitForElementToBeRemoved(() => queryByText('input/button'));
55
+ });
56
+
57
+ it('should keep the highlighter, when hovering on it (even when moving out of the component zone)', async () => {
58
+ const { getByText, findByText } = render(
59
+ <HoverHighlighter debounceSelection={debounceTime}>
60
+ <ButtonComponent>hover here</ButtonComponent>
61
+ </HoverHighlighter>
62
+ );
63
+ const rendered = getByText('hover here');
64
+
65
+ act(() => {
66
+ // hover on target element:
67
+ fireEvent.mouseOver(rendered);
68
+ });
69
+
70
+ const highlightBubble = await findByText('input/button');
71
+
72
+ await act(async () => {
73
+ // move mouse out of target element, "towards" the highlighter bubble
74
+ // this should trigger hiding
75
+ fireEvent.mouseOut(rendered);
76
+
77
+ // move mouse into the highlighter bubble
78
+ fireEvent.mouseEnter(highlightBubble);
79
+ // allow react to update state during the act()
80
+ // and before verifying highlighter remains
81
+ await new Promise((resolve) => setTimeout(resolve, debounceTime + 10));
82
+ });
83
+
84
+ // highlighter should still focus the target button
85
+ expect(await findByText('input/button')).toBeInstanceOf(HTMLElement);
86
+ });
87
+
88
+ it('should hide the highlighter when moving the mouse away of it', async () => {
89
+ const { getByText, queryByText, findByText } = render(
90
+ <HoverHighlighter debounceSelection={debounceTime}>
91
+ <ButtonComponent>hover here</ButtonComponent>
92
+ </HoverHighlighter>
93
+ );
94
+
95
+ const rendered = getByText('hover here');
96
+
97
+ // hover on target element:
98
+ await act(async () => {
99
+ fireEvent.mouseOver(rendered);
100
+ });
101
+
102
+ const highlightBubble = await findByText('input/button');
103
+
104
+ await act(async () => {
105
+ // hover on highlighter
106
+ fireEvent.mouseEnter(highlightBubble);
107
+ // leave the highlighter
108
+ fireEvent.mouseOut(highlightBubble);
109
+ await new Promise((resolve) => setTimeout(resolve, debounceTime + 10));
110
+ });
111
+
112
+ // highlighter sometimes disappears before this check,
113
+ // so not using waitForElementToBeRemoved, and using setTimeout instead
114
+ expect(queryByText('input/button')).toBeNull();
115
+ });
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { HybridHighlighter, HybridHighlighterProps } from '../hybrid-highlighter';
3
+
4
+ export type HoverHighlighterProps = Omit<HybridHighlighterProps, 'mode'>;
5
+
6
+ export function HoverHighlighter({ ...props }: HoverHighlighterProps) {
7
+ return <HybridHighlighter {...props} mode={'hover'} />;
8
+ }
@@ -0,0 +1,5 @@
1
+ export { useHoverHighlighter } from './use-hover-highlighter';
2
+ export type { useHoverHighlighterOptions } from './use-hover-highlighter';
3
+
4
+ export { HoverHighlighter } from './hover-highlighter';
5
+ export type { HoverHighlighterProps } from './hover-highlighter';
@@ -0,0 +1,85 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useDebouncedCallback } from 'use-debounce';
3
+ import { useHoverSelection } from '@teambit/react.ui.hover-selector';
4
+ import { ComponentMetaHolder } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
5
+
6
+ import { excludeHighlighterSelector, skipHighlighterSelector } from '../ignore-highlighter';
7
+ import { MatchRule, ComponentMatchRule } from '../rule-matcher';
8
+ import { bubbleToComponent } from './bubble-to-component';
9
+
10
+ type HighlightTarget = { element: HTMLElement; components: ComponentMetaHolder[] };
11
+ export type useHoverHighlighterOptions = {
12
+ debounceDuration: number;
13
+ scopeClass: string;
14
+ disabled?: boolean;
15
+ /** filter highlighter targets by this query selector. (May be a more complex object in the future) */
16
+ rule?: MatchRule;
17
+ /** filter targets by this component match rule */
18
+ componentRule?: ComponentMatchRule;
19
+ };
20
+
21
+ /** fires onChange when targeting a new component */
22
+ export function useHoverHighlighter<T extends HTMLElement = HTMLElement>(
23
+ onChange: (target?: HighlightTarget) => void,
24
+ props: React.HTMLAttributes<T> = {},
25
+ { debounceDuration, scopeClass, disabled, rule, componentRule }: useHoverHighlighterOptions
26
+ ) {
27
+ const { handleElement } = useHoverHandler({ onChange, scopeClass, debounceDuration, disabled, rule, componentRule });
28
+
29
+ const handlers = useHoverSelection(disabled ? undefined : handleElement, props);
30
+
31
+ return handlers;
32
+ }
33
+
34
+ type useHoverHighlighterProps = {
35
+ onChange: (target?: HighlightTarget) => void;
36
+ scopeClass?: string;
37
+ debounceDuration?: number;
38
+ disabled?: boolean;
39
+ rule?: MatchRule;
40
+ componentRule?: ComponentMatchRule;
41
+ };
42
+
43
+ function useHoverHandler({
44
+ onChange,
45
+ scopeClass = '',
46
+ debounceDuration,
47
+ disabled,
48
+ rule,
49
+ componentRule,
50
+ }: useHoverHighlighterProps) {
51
+ // debounced method is ref'ed, so no need for useCallback
52
+ const _handleElement = (element: HTMLElement | null) => {
53
+ // clear highlighter at the edges:
54
+ if (!element || element.hasAttribute('data-nullify-component-highlight')) {
55
+ onChange(undefined);
56
+ return;
57
+ }
58
+
59
+ // clear when ancestor has 'data-ignore-component-highlight'
60
+ if (element.closest(`.${scopeClass} ${excludeHighlighterSelector}`)) {
61
+ onChange(undefined);
62
+ return;
63
+ }
64
+
65
+ // skip DOM trees having 'data-skip-component-highlight'
66
+ if (element.closest(`.${scopeClass} ${skipHighlighterSelector}`)) return;
67
+
68
+ const result = bubbleToComponent(element, { elementRule: rule, componentRule });
69
+ if (!result) return;
70
+
71
+ onChange({
72
+ element: result.element,
73
+ components: result.components,
74
+ });
75
+ };
76
+
77
+ const handleElement = useDebouncedCallback(_handleElement, debounceDuration);
78
+
79
+ // clear when disabling
80
+ useEffect(() => {
81
+ if (disabled) handleElement.cancel();
82
+ }, [disabled, handleElement]);
83
+
84
+ return { handleElement };
85
+ }
@@ -0,0 +1,136 @@
1
+ import React, { useState, useEffect, useMemo, useRef, createRef, CSSProperties } from 'react';
2
+ import classnames from 'classnames';
3
+ import { v4 } from 'uuid';
4
+
5
+ import { ComponentMetaHolder } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
6
+
7
+ import { useHoverHighlighter } from '../hover-highlighter';
8
+ import { ElementHighlighter, Placement, HighlightClasses } from '../element-highlighter';
9
+ import { useChildrenHighlighter } from '../children-highlighter';
10
+ import type { MatchRule, ComponentMatchRule } from '../rule-matcher';
11
+
12
+ type HighlightTarget = { element: HTMLElement; components: ComponentMetaHolder[] };
13
+
14
+ export interface HybridHighlighterProps extends React.HTMLAttributes<HTMLDivElement> {
15
+ /** stop all highlighting and drop listeners */
16
+ disabled?: boolean;
17
+ /** default pop location for the label */
18
+ placement?: Placement;
19
+ /** customize styles */
20
+ classes?: HighlightClasses;
21
+ /** customize highlighter */
22
+ highlightStyle?: CSSProperties;
23
+ /** debounces element hover selection.
24
+ * A higher value will reduce element lookups as well as "keep" the highlight on the current element for longer.
25
+ * Initial selection (when no element is currently selected) will always happen immediately to improve the user experience.
26
+ * @default 80ms
27
+ */
28
+ debounceSelection?: number;
29
+ /** continually update frame position to match moving elements */
30
+ watchMotion?: boolean;
31
+
32
+ /** filter highlighter targets by this query selector. (May be a more complex object in the future) */
33
+ rule?: MatchRule;
34
+ /** filter components to match this rule. Can be id, array of ids, or a function */
35
+ componentRule?: ComponentMatchRule;
36
+
37
+ /** set the behavior of the highlighter.
38
+ * `disabled` - stops highlighting.
39
+ * `allChildren` - highlights all components rendered under children
40
+ * `hover` - highlighters the component immediately under the mouse cursor
41
+ * */
42
+ mode?: 'allChildren' | 'hover';
43
+ bgColor?: string;
44
+ bgColorHover?: string;
45
+ bgColorActive?: string;
46
+ }
47
+
48
+ /** automatically highlight components on hover */
49
+ export function HybridHighlighter({
50
+ disabled,
51
+ mode = 'hover',
52
+ debounceSelection = 80,
53
+ watchMotion = true,
54
+ placement,
55
+ rule,
56
+ componentRule,
57
+
58
+ classes,
59
+ highlightStyle,
60
+ className,
61
+ style,
62
+ bgColor,
63
+ bgColorHover,
64
+ bgColorActive,
65
+ children,
66
+ ...rest
67
+ }: HybridHighlighterProps) {
68
+ const ref = createRef<HTMLDivElement>();
69
+ const [targets, setTarget] = useState<Record<string, HighlightTarget>>({});
70
+ const scopeClass = useRef(`hl-scope-${v4()}`).current;
71
+ const hasTargets = Object.entries(targets).length > 0;
72
+
73
+ // clear targets when disabled
74
+ useEffect(() => {
75
+ if (disabled) setTarget({});
76
+ }, [disabled]);
77
+
78
+ const handlers = useHoverHighlighter(
79
+ (nextTarget) => setTarget(nextTarget ? { 'hover-target': nextTarget } : {}),
80
+ rest,
81
+ {
82
+ debounceDuration: hasTargets ? debounceSelection : 0,
83
+ scopeClass,
84
+ disabled: disabled || mode !== 'hover',
85
+ rule,
86
+ componentRule,
87
+ }
88
+ );
89
+
90
+ useChildrenHighlighter({
91
+ onChange: setTarget,
92
+ scopeRef: ref,
93
+ scopeClass,
94
+ disabled: disabled || mode !== 'allChildren',
95
+ rule,
96
+ componentRule,
97
+ });
98
+
99
+ const _styles = useMemo(
100
+ () => ({
101
+ '--bit-highlighter-color': bgColor,
102
+ '--bit-highlighter-color-hover': bgColorHover,
103
+ '--bit-highlighter-color-active': bgColorActive,
104
+ ...style,
105
+ }),
106
+ [bgColor, bgColorHover, bgColorActive, style]
107
+ );
108
+
109
+ return (
110
+ <div
111
+ ref={ref}
112
+ {...rest}
113
+ {...handlers}
114
+ style={_styles}
115
+ className={classnames(className, scopeClass)}
116
+ data-nullify-component-highlight
117
+ >
118
+ {children}
119
+ {/*
120
+ * keep the highlighter inside of the hover selector, or it could disappear when switching between elements
121
+ * the excludeHighlighterAtt will ensure it doesn't turn into a recursion.
122
+ */}
123
+ {Object.entries(targets).map(([key, target]) => (
124
+ <ElementHighlighter
125
+ key={key}
126
+ targetRef={{ current: target.element }}
127
+ components={target.components}
128
+ classes={classes}
129
+ style={highlightStyle}
130
+ placement={placement}
131
+ watchMotion={watchMotion}
132
+ />
133
+ ))}
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,2 @@
1
+ export { HybridHighlighter } from './hybrid-highlighter';
2
+ export type { HybridHighlighterProps } from './hybrid-highlighter';
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+
3
+ /** name of ignore attribute */
4
+ export const excludeHighlighterAttrName = 'data-ignore-component-highlight';
5
+
6
+ /** selector for elements with the ignore attribute */
7
+ export const excludeHighlighterSelector = `[${excludeHighlighterAttrName}]`;
8
+
9
+ /** highlighter will exclude elements with this attribute */
10
+ export const excludeHighlighterAtt = { [excludeHighlighterAttrName]: true };
11
+
12
+ /** children of this element will be excluded by the automatic highlighter */
13
+ export function ExcludeHighlighter(props: React.HTMLAttributes<HTMLDivElement>) {
14
+ return <div {...props} {...excludeHighlighterAtt} />;
15
+ }
16
+
17
+ /** name of skip attribute */
18
+ export const skipHighlighterAttrName = 'data-skip-component-highlight';
19
+ /** highlighter will skip (ignore) elements with these attributes */
20
+ export const skipHighlighterAttr = { [skipHighlighterAttrName]: true };
21
+ /** selector for elements with the skip attribute */
22
+ export const skipHighlighterSelector = `[${skipHighlighterAttrName}]`;
package/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export { HybridHighlighter as ComponentHighlighter } from './hybrid-highlighter';
2
+ export type { HybridHighlighterProps as ComponentHighlightProps } from './hybrid-highlighter';
3
+
4
+ export { HoverHighlighter } from './hover-highlighter';
5
+ export type { HoverHighlighterProps } from './hover-highlighter';
6
+
7
+ export { ChildrenHighlighter } from './children-highlighter';
8
+ export type { ChildrenHighlighterProps } from './children-highlighter';
9
+
10
+ export { ElementHighlighter } from './element-highlighter';
11
+ export type { ElementHighlighterProps, Placement, HighlightClasses } from './element-highlighter';
12
+
13
+ export {
14
+ ExcludeHighlighter,
15
+ excludeHighlighterAtt,
16
+ excludeHighlighterAttrName,
17
+ skipHighlighterAttr,
18
+ skipHighlighterAttrName,
19
+ } from './ignore-highlighter';
20
+
21
+ export type { MatchRule } from './rule-matcher';
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { ComponentStrip } from './component-strip';
3
+ import { MockButton, MockSnap } from '../mock-component';
4
+
5
+ export const ComponentStripPreview = () => {
6
+ return (
7
+ <div style={{ fontFamily: 'sans-serif', padding: 8 }}>
8
+ <ComponentStrip component={MockSnap} />
9
+ <br />
10
+ <ComponentStrip component={MockButton} />
11
+ </div>
12
+ );
13
+ };
@@ -0,0 +1,68 @@
1
+ $borderRadius: 0.5em;
2
+ $gap: 0.125em;
3
+
4
+ .componentStrip {
5
+ display: flex;
6
+ width: fit-content; // for correct shadow size
7
+
8
+ border-radius: $borderRadius;
9
+ box-shadow: var(--bit-highlighter-shadow);
10
+ white-space: nowrap;
11
+
12
+ > * {
13
+ padding: 0 0.5em;
14
+ line-height: 1.5; //use line height to get rounder values than 0.25em padding
15
+
16
+ transition: filter 300ms, background-color 300ms;
17
+ transform: translateZ(0); //fix blurriness in Safari
18
+
19
+ background: var(--bit-highlighter-color, #eebcc9);
20
+
21
+ margin-right: $gap;
22
+
23
+ &:link,
24
+ &:visited {
25
+ text-decoration: inherit; // reset browser defaults
26
+ color: inherit; // reset browser defaults
27
+
28
+ &:hover {
29
+ background: var(--bit-highlighter-color-hover, #f6dae2);
30
+ }
31
+
32
+ &:active {
33
+ background: var(--bit-highlighter-color-active, #e79db1);
34
+ color: inherit;
35
+ }
36
+ }
37
+
38
+ &:first-child {
39
+ border-top-left-radius: $borderRadius;
40
+ border-bottom-left-radius: $borderRadius;
41
+ }
42
+
43
+ &:last-child {
44
+ border-top-right-radius: $borderRadius;
45
+ border-bottom-right-radius: $borderRadius;
46
+
47
+ margin-right: unset;
48
+ }
49
+ }
50
+ }
51
+
52
+ .nameBlock {
53
+ display: flex;
54
+
55
+ .version {
56
+ // leave room for 9 digits + 3 "."
57
+ max-width: 13ch;
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ white-space: nowrap;
61
+
62
+ transition: max-width 480ms;
63
+
64
+ &:hover {
65
+ max-width: 61ch;
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,57 @@
1
+ import React, { useMemo, ReactNode, forwardRef, ForwardedRef } from 'react';
2
+ import { NativeLink } from '@teambit/base-ui.routing.native-link';
3
+ import { ComponentID } from '@teambit/component-id';
4
+ import { ScopeUrl } from '@teambit/component.modules.component-url';
5
+ import {
6
+ ComponentMeta,
7
+ componentMetaField,
8
+ ComponentMetaHolder,
9
+ } from '@teambit/react.ui.highlighter.component-metadata.bit-component-meta';
10
+ import styles from './component-strip.module.scss';
11
+ import { calcComponentLink } from './links';
12
+
13
+ interface ComponentStripProps extends React.HTMLAttributes<HTMLDivElement> {
14
+ component: ComponentMetaHolder | string;
15
+ }
16
+ export const ComponentStrip = forwardRef(function ComponentStrip(
17
+ { component, children }: ComponentStripProps,
18
+ ref: ForwardedRef<HTMLDivElement>
19
+ ) {
20
+ const { id, homepage, exported } = extractMetadata(component);
21
+
22
+ const parsedId = useMemo(() => ComponentID.tryFromString(id), [id]);
23
+ const componentLink = homepage || calcComponentLink(parsedId, exported);
24
+
25
+ return (
26
+ <div className={styles.componentStrip} ref={ref}>
27
+ {!parsedId && <LabelBlock link={homepage}>{id}</LabelBlock>}
28
+ {parsedId && <LabelBlock link={ScopeUrl.toUrl(parsedId.scope)}>{parsedId.scope}</LabelBlock>}
29
+ {parsedId && (
30
+ <LabelBlock link={componentLink} className={styles.nameBlock}>
31
+ <span>{parsedId.fullName}</span>
32
+ {parsedId.version && parsedId.version !== 'latest' && (
33
+ <span className={styles.version}>@{parsedId.version}</span>
34
+ )}
35
+ </LabelBlock>
36
+ )}
37
+ {children}
38
+ </div>
39
+ );
40
+ });
41
+
42
+ function LabelBlock({ link, children, className }: { link?: string; children: ReactNode; className?: string }) {
43
+ const Comp = link ? NativeLink : 'span';
44
+ return (
45
+ <Comp href={link} external={!!link} className={className}>
46
+ {children}
47
+ </Comp>
48
+ );
49
+ }
50
+
51
+ function extractMetadata(metadata: string | ComponentMetaHolder): ComponentMeta {
52
+ if (typeof metadata === 'string') {
53
+ return { id: metadata, exported: true };
54
+ }
55
+
56
+ return metadata[componentMetaField];
57
+ }
package/label/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { Label } from './label';
2
+ export type { LabelProps } from './label';
3
+
4
+ export { LabelContainer } from './label-container';
5
+ export type { LabelContainerProps, Placement } from './label-container';
@@ -0,0 +1,72 @@
1
+ import React, { useLayoutEffect, useEffect, RefObject } from 'react';
2
+ import classnames from 'classnames';
3
+ import compact from 'lodash.compact';
4
+ import {
5
+ useFloating,
6
+ offset as offsetMiddleware,
7
+ flip as flipMiddleware,
8
+ shift,
9
+ autoUpdate,
10
+ hide,
11
+ } from '@floating-ui/react-dom';
12
+ import type { Placement } from '@floating-ui/react-dom';
13
+ import styles from './label.module.scss';
14
+
15
+ const HAS_RESIZE_OBSERVER = typeof window !== 'undefined' && !!window.ResizeObserver;
16
+
17
+ export interface LabelContainerProps extends React.HTMLAttributes<HTMLDivElement> {
18
+ targetRef: RefObject<HTMLElement | null>;
19
+ offset?: [number, number];
20
+ placement?: Placement;
21
+ flip?: boolean;
22
+ /** continually update label position to match moving elements */
23
+ watchMotion?: boolean;
24
+ }
25
+
26
+ export type { Placement };
27
+
28
+ export function LabelContainer({
29
+ targetRef,
30
+ offset,
31
+ placement,
32
+ flip = true,
33
+ watchMotion,
34
+ className,
35
+ style,
36
+ ...rest
37
+ }: LabelContainerProps) {
38
+ const { x, y, strategy, floating, reference, refs, update, middlewareData } = useFloating({
39
+ placement,
40
+ middleware: compact([
41
+ offset && offsetMiddleware({ mainAxis: offset[0], crossAxis: offset[1] }),
42
+ flip && flipMiddleware(),
43
+ // enabling 'shift' for 'crossAxis' will make floating-ui push the label _inside_, when it has nowhere to go
44
+ shift({ rootBoundary: 'document', mainAxis: true, crossAxis: true }),
45
+ hide({ strategy: 'referenceHidden' }),
46
+ ]),
47
+ });
48
+
49
+ useLayoutEffect(() => {
50
+ reference(targetRef.current);
51
+ }, [targetRef.current, reference]);
52
+
53
+ // automatically update on scroll, resize, etc.
54
+ // `watchMotion` will trigger continuous updates using animation frame
55
+ useEffect(() => {
56
+ if (!refs.reference.current || !refs.floating.current || !HAS_RESIZE_OBSERVER) return () => {};
57
+
58
+ return autoUpdate(refs.reference.current, refs.floating.current, update, { animationFrame: !!watchMotion });
59
+ }, [refs.reference.current, refs.floating.current, update, watchMotion]);
60
+
61
+ const isReady = !middlewareData.hide?.referenceHidden;
62
+
63
+ return (
64
+ <div
65
+ {...rest}
66
+ ref={floating}
67
+ className={classnames(className, !isReady && styles.hidden)}
68
+ // starting at pos [0,0] will ensure the label doesn't increase the document size.
69
+ style={{ ...style, position: strategy, top: y ?? 0, left: x ?? 0 }}
70
+ />
71
+ );
72
+ }
@@ -0,0 +1,32 @@
1
+ .othersContainer {
2
+ > * {
3
+ margin-bottom: 8px;
4
+
5
+ &:last-child {
6
+ margin-bottom: unset;
7
+ }
8
+ }
9
+ }
10
+
11
+ .othersTooltip {
12
+ user-select: none;
13
+ cursor: pointer;
14
+
15
+ &::before {
16
+ display: inline-block;
17
+ transition: transform 300ms;
18
+ content: '▾';
19
+ }
20
+
21
+ &.active::before {
22
+ transform: rotate(-180deg);
23
+ }
24
+ }
25
+
26
+ .hidden {
27
+ // label size is needed for position calculations,
28
+ // so it can't be removed by `display: none`
29
+ visibility: hidden;
30
+ pointer-events: none;
31
+ user-select: none;
32
+ }