@ldelia/react-media 0.2.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/.bitmap +48 -0
- package/.eslintcache +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +7 -0
- package/.storybook/main.js +15 -0
- package/.storybook/main.ts +18 -0
- package/.storybook/preview.js +4 -0
- package/.storybook/preview.ts +14 -0
- package/README.md +26 -0
- package/package.json +87 -0
- package/src/components/__tests__/timeline/timeline.test.tsx +26 -0
- package/src/components/index.ts +1 -0
- package/src/components/timeline/README.md +111 -0
- package/src/components/timeline/RangeSelectorCanvas.tsx +168 -0
- package/src/components/timeline/TickTime.tsx +45 -0
- package/src/components/timeline/TickTimeCollectionDisplay.tsx +42 -0
- package/src/components/timeline/VaLueLineCanvas.tsx +101 -0
- package/src/components/timeline/constants.ts +13 -0
- package/src/components/timeline/index.tsx +125 -0
- package/src/components/timeline/utils/utils.ts +16 -0
- package/src/index.ts +1 -0
- package/src/modules.d.ts +1 -0
- package/src/react-app-env.d.ts +1 -0
- package/src/reportWebVitals.ts +15 -0
- package/src/setupTests.ts +5 -0
- package/src/stories/Button.stories.ts +52 -0
- package/src/stories/Button.tsx +52 -0
- package/src/stories/Configure.mdx +364 -0
- package/src/stories/button.css +30 -0
- package/src/stories/timeline.stories.tsx +38 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import { ZoomContext, ZoomContextType } from './index';
|
|
5
|
+
import { useCallback, useContext, useEffect, useRef } from 'react';
|
|
6
|
+
import { secondsToPixel } from './utils/utils';
|
|
7
|
+
|
|
8
|
+
export interface VaLueLineCanvasProps {
|
|
9
|
+
blockStartingTimes: number[];
|
|
10
|
+
value: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const OverlayCanvas = styled.canvas`
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 0;
|
|
16
|
+
left: 0;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
color: #c9c9c9;
|
|
20
|
+
`;
|
|
21
|
+
const ValueLine = styled.span`
|
|
22
|
+
position: absolute;
|
|
23
|
+
width: 1px;
|
|
24
|
+
height: 100%;
|
|
25
|
+
background-color: #575757;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const VaLueLineCanvas: React.FC<VaLueLineCanvasProps> = ({
|
|
29
|
+
blockStartingTimes = [],
|
|
30
|
+
value,
|
|
31
|
+
}) => {
|
|
32
|
+
const canvasRef = useRef(null);
|
|
33
|
+
const valueLineRef = useRef(null);
|
|
34
|
+
const zoomContextValue: ZoomContextType = useContext(ZoomContext);
|
|
35
|
+
|
|
36
|
+
const showBlocks = useCallback(
|
|
37
|
+
(canvas: HTMLCanvasElement) => {
|
|
38
|
+
const blockHeight = 20;
|
|
39
|
+
const context: CanvasRenderingContext2D = canvas.getContext('2d')!;
|
|
40
|
+
|
|
41
|
+
for (const blockStartingTime of blockStartingTimes) {
|
|
42
|
+
const x0Pixel = secondsToPixel(zoomContextValue, blockStartingTime);
|
|
43
|
+
const x1Pixel = secondsToPixel(
|
|
44
|
+
zoomContextValue,
|
|
45
|
+
blockStartingTime + zoomContextValue.blockOffset,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
context.beginPath();
|
|
49
|
+
context.moveTo(x0Pixel, canvas.height);
|
|
50
|
+
context.lineTo(x0Pixel, canvas.height - blockHeight);
|
|
51
|
+
context.lineTo(x1Pixel, canvas.height - blockHeight);
|
|
52
|
+
context.lineTo(x1Pixel, canvas.height);
|
|
53
|
+
|
|
54
|
+
context.strokeStyle = window
|
|
55
|
+
.getComputedStyle(canvas)
|
|
56
|
+
.getPropertyValue('color');
|
|
57
|
+
context.stroke();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[blockStartingTimes, zoomContextValue],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const showValueLine = useCallback(() => {
|
|
64
|
+
const linePosition: number = secondsToPixel(zoomContextValue, value);
|
|
65
|
+
|
|
66
|
+
const valueLineElement: HTMLElement = valueLineRef.current!;
|
|
67
|
+
const elementWidthCSSProperty: string = window
|
|
68
|
+
.getComputedStyle(valueLineElement)
|
|
69
|
+
.getPropertyValue('width')
|
|
70
|
+
.replace(/[^-\d]/g, '');
|
|
71
|
+
const elementWidth: number = parseInt(elementWidthCSSProperty);
|
|
72
|
+
const linePositionAtValueLineMiddle: number =
|
|
73
|
+
linePosition - elementWidth / 2;
|
|
74
|
+
valueLineElement.style.left = linePositionAtValueLineMiddle + 'px';
|
|
75
|
+
}, [value, zoomContextValue]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const canvas: HTMLCanvasElement = canvasRef.current!;
|
|
79
|
+
|
|
80
|
+
// https://stackoverflow.com/questions/8696631/canvas-drawings-like-lines-are-blurry
|
|
81
|
+
canvas.width = canvas.offsetWidth;
|
|
82
|
+
canvas.height = canvas.offsetHeight;
|
|
83
|
+
|
|
84
|
+
showBlocks(canvas);
|
|
85
|
+
showValueLine();
|
|
86
|
+
}, [showBlocks, showValueLine]);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<>
|
|
90
|
+
<OverlayCanvas
|
|
91
|
+
ref={canvasRef}
|
|
92
|
+
className={'media-timeline-value-line-canvas'}
|
|
93
|
+
/>
|
|
94
|
+
<ValueLine
|
|
95
|
+
ref={valueLineRef}
|
|
96
|
+
className={'media-timeline-value-line'}
|
|
97
|
+
></ValueLine>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
export default VaLueLineCanvas;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//[blockOffset, pixelsInSecond] when zoomLevel === 0, each block has an offset of 20 seconds, and each second has a width of 7px.
|
|
2
|
+
export const zoomLevelConfigurations: number[][] = [
|
|
3
|
+
[20, 7],
|
|
4
|
+
[10, 10],
|
|
5
|
+
[10, 15],
|
|
6
|
+
[5, 20],
|
|
7
|
+
[5, 25],
|
|
8
|
+
[2, 35],
|
|
9
|
+
[2, 50],
|
|
10
|
+
[1, 60],
|
|
11
|
+
[1, 75],
|
|
12
|
+
[1, 90],
|
|
13
|
+
];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
4
|
+
import TickTimeCollectionDisplay from './TickTimeCollectionDisplay';
|
|
5
|
+
import VaLueLineCanvas from './VaLueLineCanvas';
|
|
6
|
+
import RangeSelectorCanvas from './RangeSelectorCanvas';
|
|
7
|
+
import { zoomLevelConfigurations } from './constants';
|
|
8
|
+
|
|
9
|
+
const TimelineContainer = styled.div`
|
|
10
|
+
background-color: #f0f0f0;
|
|
11
|
+
border: 1px solid #c9c9c9;
|
|
12
|
+
height: 80px;
|
|
13
|
+
width: 100%;
|
|
14
|
+
max-width: 100%;
|
|
15
|
+
overflow-x: auto;
|
|
16
|
+
position: relative;
|
|
17
|
+
display: flex;
|
|
18
|
+
`;
|
|
19
|
+
const TimelineWrapper = styled.div`
|
|
20
|
+
position: absolute;
|
|
21
|
+
height: 100%;
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export interface TimelineProps {
|
|
25
|
+
duration: number;
|
|
26
|
+
value: number;
|
|
27
|
+
zoomLevel?: number; //0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
28
|
+
className?: string;
|
|
29
|
+
selectedRange?: number[];
|
|
30
|
+
onChange?: (value: number) => void;
|
|
31
|
+
onRangeChange?: (value: number[]) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ZoomContextType = {
|
|
35
|
+
blockOffset: number;
|
|
36
|
+
pixelsInSecond: number;
|
|
37
|
+
};
|
|
38
|
+
export const ZoomContext = React.createContext<ZoomContextType>({
|
|
39
|
+
blockOffset: 0,
|
|
40
|
+
pixelsInSecond: 0,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const Timeline: React.FC<TimelineProps> = ({
|
|
44
|
+
duration,
|
|
45
|
+
value,
|
|
46
|
+
zoomLevel = 0,
|
|
47
|
+
selectedRange = [],
|
|
48
|
+
onChange = () => {},
|
|
49
|
+
onRangeChange = () => {},
|
|
50
|
+
className = '',
|
|
51
|
+
}) => {
|
|
52
|
+
const timeLineContainerRef = useRef(null);
|
|
53
|
+
|
|
54
|
+
let zoomLevelValue = zoomLevel ? zoomLevel : 0;
|
|
55
|
+
if (zoomLevelValue < 0 || zoomLevelValue >= zoomLevelConfigurations.length) {
|
|
56
|
+
console.warn('Invalid value for property zoomLevel.');
|
|
57
|
+
zoomLevelValue = 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (value < 0 || value > duration) {
|
|
61
|
+
console.warn('Invalid value.');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!(selectedRange.length === 0 || selectedRange.length === 2)) {
|
|
65
|
+
selectedRange = [];
|
|
66
|
+
console.warn('The selected range must contain only two values.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
selectedRange.length === 2 &&
|
|
71
|
+
!(
|
|
72
|
+
0 <= selectedRange[0] &&
|
|
73
|
+
selectedRange[0] < selectedRange[1] &&
|
|
74
|
+
selectedRange[1] <= duration
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
selectedRange = [];
|
|
78
|
+
console.warn('The selected range is inconsistent.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const zoomParams = useMemo(() => {
|
|
82
|
+
return {
|
|
83
|
+
blockOffset: zoomLevelConfigurations[zoomLevelValue][0],
|
|
84
|
+
pixelsInSecond: zoomLevelConfigurations[zoomLevelValue][1],
|
|
85
|
+
};
|
|
86
|
+
}, [zoomLevelValue]);
|
|
87
|
+
|
|
88
|
+
let blockStartingTimes = [0];
|
|
89
|
+
const blockCounts: number = Math.ceil(duration / zoomParams.blockOffset);
|
|
90
|
+
for (let i: number = 1; i < blockCounts; i++) {
|
|
91
|
+
blockStartingTimes.push(
|
|
92
|
+
blockStartingTimes[blockStartingTimes.length - 1] +
|
|
93
|
+
zoomParams.blockOffset,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const timeLineWrapper: HTMLElement = timeLineContainerRef.current!;
|
|
99
|
+
const scrollPosition: number = value * zoomParams.pixelsInSecond - 300;
|
|
100
|
+
timeLineWrapper.scrollLeft = Math.max(0, scrollPosition);
|
|
101
|
+
}, [value, zoomParams]);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<TimelineContainer ref={timeLineContainerRef} className={className}>
|
|
105
|
+
<TimelineWrapper
|
|
106
|
+
style={{ width: duration * zoomParams.pixelsInSecond + 'px' }}
|
|
107
|
+
>
|
|
108
|
+
<ZoomContext.Provider value={zoomParams}>
|
|
109
|
+
<VaLueLineCanvas
|
|
110
|
+
blockStartingTimes={blockStartingTimes}
|
|
111
|
+
value={value}
|
|
112
|
+
/>
|
|
113
|
+
<RangeSelectorCanvas
|
|
114
|
+
selectedRange={selectedRange}
|
|
115
|
+
onChange={onChange}
|
|
116
|
+
onRangeChange={onRangeChange}
|
|
117
|
+
/>
|
|
118
|
+
<TickTimeCollectionDisplay tickTimes={blockStartingTimes} />
|
|
119
|
+
</ZoomContext.Provider>
|
|
120
|
+
</TimelineWrapper>
|
|
121
|
+
</TimelineContainer>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default Timeline;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ZoomContextType } from '../index';
|
|
2
|
+
|
|
3
|
+
export const secondsToPixel = (
|
|
4
|
+
zoomContextValue: ZoomContextType,
|
|
5
|
+
seconds: number,
|
|
6
|
+
) => {
|
|
7
|
+
return zoomContextValue.pixelsInSecond * seconds;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const pixelToSeconds = (
|
|
11
|
+
zoomContextValue: ZoomContextType,
|
|
12
|
+
pixel: number,
|
|
13
|
+
) => {
|
|
14
|
+
const seconds = pixel / zoomContextValue.pixelsInSecond;
|
|
15
|
+
return Math.round((seconds + Number.EPSILON) * 100) / 100;
|
|
16
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components';
|
package/src/modules.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.md';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="react-scripts" />
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ReportHandler } from 'web-vitals';
|
|
2
|
+
|
|
3
|
+
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
|
4
|
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
5
|
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
6
|
+
getCLS(onPerfEntry);
|
|
7
|
+
getFID(onPerfEntry);
|
|
8
|
+
getFCP(onPerfEntry);
|
|
9
|
+
getLCP(onPerfEntry);
|
|
10
|
+
getTTFB(onPerfEntry);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default reportWebVitals;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { fn } from '@storybook/test';
|
|
3
|
+
import { Button } from './Button';
|
|
4
|
+
|
|
5
|
+
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Example/Button',
|
|
8
|
+
component: Button,
|
|
9
|
+
parameters: {
|
|
10
|
+
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
|
|
11
|
+
layout: 'centered',
|
|
12
|
+
},
|
|
13
|
+
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
// More on argTypes: https://storybook.js.org/docs/api/argtypes
|
|
16
|
+
argTypes: {
|
|
17
|
+
backgroundColor: { control: 'color' },
|
|
18
|
+
},
|
|
19
|
+
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
|
|
20
|
+
args: { onClick: fn() },
|
|
21
|
+
} satisfies Meta<typeof Button>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
|
|
27
|
+
export const Primary: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
primary: true,
|
|
30
|
+
label: 'Button',
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Secondary: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
label: 'Button',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Large: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
size: 'large',
|
|
43
|
+
label: 'Button',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Small: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
size: 'small',
|
|
50
|
+
label: 'Button',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './button.css';
|
|
3
|
+
|
|
4
|
+
interface ButtonProps {
|
|
5
|
+
/**
|
|
6
|
+
* Is this the principal call to action on the page?
|
|
7
|
+
*/
|
|
8
|
+
primary?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* What background color to use
|
|
11
|
+
*/
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
/**
|
|
14
|
+
* How large should the button be?
|
|
15
|
+
*/
|
|
16
|
+
size?: 'small' | 'medium' | 'large';
|
|
17
|
+
/**
|
|
18
|
+
* Button contents
|
|
19
|
+
*/
|
|
20
|
+
label: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional click handler
|
|
23
|
+
*/
|
|
24
|
+
onClick?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Primary UI component for user interaction
|
|
29
|
+
*/
|
|
30
|
+
export const Button = ({
|
|
31
|
+
primary = false,
|
|
32
|
+
size = 'medium',
|
|
33
|
+
backgroundColor,
|
|
34
|
+
label,
|
|
35
|
+
...props
|
|
36
|
+
}: ButtonProps) => {
|
|
37
|
+
const mode = primary
|
|
38
|
+
? 'storybook-button--primary'
|
|
39
|
+
: 'storybook-button--secondary';
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
className={['storybook-button', `storybook-button--${size}`, mode].join(
|
|
44
|
+
' ',
|
|
45
|
+
)}
|
|
46
|
+
style={{ backgroundColor }}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{label}
|
|
50
|
+
</button>
|
|
51
|
+
);
|
|
52
|
+
};
|