@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.
@@ -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';
@@ -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,5 @@
1
+ // jest-dom adds custom jest matchers for asserting on DOM nodes.
2
+ // allows you to do things like:
3
+ // expect(element).toHaveTextContent(/react/i)
4
+ // learn more: https://github.com/testing-library/jest-dom
5
+ import '@testing-library/jest-dom';
@@ -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
+ };