Skip to content

Commit 92d86dd

Browse files
authored
Merge pull request #109 from openscript-ch/82-add-soft-timeline-ends
82 add soft timeline ends
2 parents f969c0d + 201c651 commit 92d86dd

File tree

7 files changed

+151
-71
lines changed

7 files changed

+151
-71
lines changed

README.md

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# react-alternating-timeline
22

3-
A compact, masonry style alternating timeline react component which is fully customizable.
3+
A simple and compact, true masonry style alternating timeline react component which is fully customizable and free stylable.
44

55
![Demonstration](./docs/demonstration.jpg)
66

@@ -57,7 +57,7 @@ The available properties of the `Timeline` component:
5757
| `formatDate?` | `(date: Date) => string` | Callback to format date | |
5858
| `customMarker?` | `ReactElement` | Custom maker element replacing the default | |
5959
| `customPointer?` | `ReactElement` | Custom pointer element replacing the default | |
60-
| `styleConfig?` | [`StyleConfig`](#styleconfig) | Style config object for customizing timeline by setting css custom properties | |
60+
| `styleConfig?` | [`StyleConfig`](#styling) | Style config object for customizing timeline by setting css custom properties | |
6161
| `className?` | `string` | Additional class name | |
6262

6363
### TimelineItemsProps
@@ -74,65 +74,94 @@ An array of the following properties:
7474
| `customMarker?` | `ReactElement` | Overwriting `customMarker` property of parent `Timeline` |
7575
| `customPointer?` | `ReactElement` | Overwriting `customPointer` property of parent `Timeline` |
7676

77-
### StyleConfig
77+
## Styling
7878

79-
The style can either be passed as a javascript object...
79+
The style can either be passed as an object through the `styleConfig` property...
8080

8181
```ts
8282
{
8383
line?: {
84-
width?: string;
85-
color?: string;
84+
width?: CSSProperties['width'];
85+
color?: CSSProperties['backgroundColor'];
86+
overhang?: CSSProperties['paddingBlock'];
8687
};
87-
gap?: string;
88-
offset?: {
89-
left?: string;
90-
right?: string;
88+
item?: {
89+
gap?: CSSProperties['gap'];
90+
startOffset?: {
91+
left?: CSSProperties['marginTop'];
92+
right?: CSSProperties['marginTop'];
93+
};
9194
};
9295
marker?: {
93-
size?: string;
94-
color?: string;
95-
radius?: string;
96+
size?: CSSProperties['width'];
97+
color?: CSSProperties['backgroundColor'];
98+
radius?: CSSProperties['borderRadius'];
9699
};
97100
pointer?: {
98-
height?: string;
99-
width?: string;
100-
offset?: string;
101+
height?: CSSProperties['height'];
102+
width?: CSSProperties['width'];
103+
minOffset?: CSSProperties['marginTop'];
101104
};
102105
card?: {
103-
background?: string;
104-
radius?: string;
105-
offset?: string;
106-
shadow?: string;
107-
padding?: string;
106+
background?: CSSProperties['backgroundColor'];
107+
radius?: CSSProperties['borderRadius'];
108+
shadow?: CSSProperties['boxShadow'];
109+
padding?: CSSProperties['padding'];
110+
offset?: CSSProperties['gap'];
108111
};
109-
};
112+
}
110113
```
111114

112-
...or the custom properties can be set directly in css
115+
...or can be set as custom properties directly in css
113116

114117
```css
115118
.timeline {
116119
--line-width: 0.2rem;
117120
--line-color: black;
121+
--line-overhang: 1rem;
122+
--item-gap: 1rem;
123+
--item-start-offset-left: 0;
124+
--item-start-offset-right: 5rem;
118125
--marker-size: 1rem;
119126
--marker-color: var(--line-color);
120127
--marker-radius: 50%;
121128
--pointer-height: 2rem;
122129
--pointer-width: 1rem;
123-
--pointer-offset: 5rem;
130+
--pointer-min-offset: 5rem;
124131
--card-background: whitesmoke;
125132
--card-radius: 0.1rem;
126-
--card-offset: 1rem;
127133
--card-shadow: unset;
128134
--card-padding: 1rem;
129-
--gap: 1rem;
130-
--offset-left: 0;
131-
--offset-right: 5rem;
135+
--card-offset: 1rem;
132136
}
133137
```
134138

135-
(These are the default values)
139+
### StyleConfig
140+
141+
| Name | Description | Default |
142+
| :------------------------- | :------------------------------------------------------------------------------------------------------------- | :------------ |
143+
| **Line** | The line the timeline items are place around/beside | |
144+
|`line-width` | Width of the line | `0.2rem` |
145+
|`line-color` | Color of the line | `black` |
146+
|`line-overhang` | How much the line should overhang the beginning and end of the timeline component | `1rem` |
147+
| **Item** | The timeline item as a whole, including the card, pointer and marker | |
148+
|`item-gap` | The vertical space between the items | `1rem` |
149+
|`item-start-offset-left` | How much the items on the left side should be offset from the top | `0` |
150+
|`item-start-offset-left` | How much the items on the right side should be offset from the top | `5rem` |
151+
| **Marker** | The markers on the line which marks the chronological order of the timeline items | `1rem` |
152+
|`marker-size` | Size of the default marker | `1rem` |
153+
|`marker-color` | Color of the default marker | `line-color` |
154+
|`marker-radius` | Border radius (roundness) of the marker edges | `50%` (round) |
155+
| **Pointer** | The pointers pointing from the item cards to the markers on the line | |
156+
|`pointer-height` | Height of the default pointer | `2rem` |
157+
|`pointer-width` | Width of the default pointer | `1rem` |
158+
|`pointer-min-offset` | Minimum offset of the pointer to the top of the card. The actual offset depends on the `minMarkerGap` property | `5rem` |
159+
| **Card** | The cards in which the timeline item content is displayed | |
160+
|`card-background` | Background color of the card | `whitesmoke` |
161+
|`card-radius` | Border radius of the card edges | `0.1rem` |
162+
|`card-shadow` | Configure drop shadow of the card | `unset` |
163+
|`card-padding` | Padding of the card content | `1rem` |
164+
|`card-offset` | Space between the card and the timeline line | `1rem` |
136165

137166
## Demo
138167

src/components/Timeline.css

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,61 @@
11
:where(.timeline) {
22
--line-width: 0.2rem;
33
--line-color: black;
4+
--line-overhang: 1rem;
5+
--item-gap: 1rem;
6+
--item-start-offset-left: 0;
7+
--item-start-offset-right: 5rem;
48
--marker-size: 1rem;
59
--marker-color: var(--line-color);
610
--marker-radius: 50%;
711
--pointer-height: 2rem;
812
--pointer-width: 1rem;
9-
--pointer-offset: 5rem;
13+
--pointer-min-offset: 5rem;
1014
--card-background: whitesmoke;
1115
--card-radius: 0.1rem;
12-
--card-offset: 1rem;
1316
--card-shadow: unset;
1417
--card-padding: 1rem;
15-
--gap: 1rem;
16-
--offset-left: 0;
17-
--offset-right: 5rem;
18-
19-
position: relative;
18+
--card-offset: 1rem;
2019
}
2120

2221
.timeline {
2322
display: flex;
2423
}
2524

26-
.timeline__line {
25+
.timeline-line {
2726
background-color: var(--line-color);
2827
min-width: var(--line-width);
28+
position: relative;
29+
}
30+
31+
.timeline-line__end {
32+
position: absolute;
33+
left: 50%;
34+
transform: translate(-50%);
35+
}
36+
37+
.timeline-line__end--opening {
38+
top: 0;
39+
}
40+
41+
.timeline-line__end--closing {
42+
bottom: 0;
2943
}
3044

3145
.timeline__items-container {
3246
display: flex;
47+
padding-block: var(--line-overhang);
3348
flex-direction: column;
3449
flex: 1;
50+
gap: var(--item-gap);
3551
}
3652

3753
.timeline__items-container--left {
38-
margin-top: var(--offset-left);
54+
margin-top: var(--item-start-offset-left);
3955
}
4056

4157
.timeline__items-container--right {
42-
margin-top: var(--offset-right);
58+
margin-top: var(--item-start-offset-right);
4359
}
4460

4561
.timeline-item {
@@ -59,7 +75,7 @@
5975
}
6076

6177
.timeline-item__marker {
62-
margin-top: var(--pointer-offset);
78+
margin-top: var(--pointer-min-offset);
6379
}
6480

6581
.timeline-item__marker:not(.timeline-item__marker--custom) {

src/components/Timeline.stories.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,18 @@ export const SpacingOptions: StoryObj<typeof Timeline> = {
161161
items,
162162
...defaultTimelineConfig,
163163
styleConfig: {
164-
gap: '5rem',
165-
offset: {
166-
left: '10rem',
167-
right: '3rem',
164+
item: {
165+
gap: '5rem',
166+
startOffset: {
167+
left: '10rem',
168+
right: '3rem',
169+
},
168170
},
169171
pointer: {
170-
offset: '4rem',
172+
minOffset: '4rem',
173+
},
174+
line: {
175+
overhang: '4rem',
171176
},
172177
},
173178
minMarkerGap: 150,
@@ -257,8 +262,12 @@ customItems[3] = {
257262
customPointer: <div className="pointy" style={{ backgroundColor: 'green' }} />,
258263
};
259264

260-
export const CustomMarkerAndPointer: StoryObj<typeof Timeline> = {
265+
export const CustomMarkerPointerAndEnds: StoryObj<typeof Timeline> = {
261266
args: {
267+
customTimelineEnds: {
268+
opening: <div style={{ width: '1rem', height: '1rem', backgroundColor: 'green' }} />,
269+
closing: <div style={{ width: '1rem', height: '1rem', backgroundColor: 'blue' }} />,
270+
},
262271
items: customItems,
263272
...defaultTimelineConfig,
264273
customMarker: <span style={{ fontSize: '2rem' }}>🔥</span>,

src/components/Timeline.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Key, ReactElement, useEffect, useRef } from 'react';
33
import { TimelineItem, TimelineItemRefs, TimelineItemsProps } from './TimelineItem';
44
import { Positioning } from '../models/positioning';
55
import { convertToCssVariable, StyleConfig } from '../models/style';
6+
import { isElement } from '../helper/element';
67

78
export type TimelineProps = {
89
items: TimelineItemsProps;
@@ -11,6 +12,7 @@ export type TimelineProps = {
1112
formatDate?: (date: Date) => string;
1213
customMarker?: ReactElement;
1314
customPointer?: ReactElement;
15+
customTimelineEnds?: ReactElement | { opening?: ReactElement; closing?: ReactElement };
1416
styleConfig?: StyleConfig;
1517
className?: string;
1618
};
@@ -21,7 +23,7 @@ export const defaultTimelineConfig: Partial<TimelineProps> = {
2123
};
2224

2325
export function Timeline(props: TimelineProps) {
24-
const { items, positioning, minMarkerGap, className, customMarker, customPointer, styleConfig, formatDate } = {
26+
const { items, positioning, minMarkerGap, className, customMarker, customPointer, customTimelineEnds, styleConfig, formatDate } = {
2527
...defaultTimelineConfig,
2628
...props,
2729
};
@@ -128,7 +130,14 @@ export function Timeline(props: TimelineProps) {
128130
))}
129131
</div>
130132

131-
<div className="timeline__line" />
133+
<div className="timeline-line">
134+
<div className="timeline-line__end timeline-line__end--opening">
135+
{isElement(customTimelineEnds) ? customTimelineEnds : customTimelineEnds?.opening}
136+
</div>
137+
<div className="timeline-line__end timeline-line__end--closing">
138+
{isElement(customTimelineEnds) ? customTimelineEnds : customTimelineEnds?.closing}
139+
</div>
140+
</div>
132141

133142
<div ref={rightContainer} className="timeline__items-container timeline__items-container--right" />
134143
</div>

src/helper/element.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { ReactElement, isValidElement } from 'react';
2+
3+
export const isElement = (element: any): element is ReactElement => isValidElement(element);

src/helper/object.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
export const flattenObject = <O extends Record<string, T>, T extends O | string | undefined>(ob: Record<string, T>, delimiter?: string) => {
1+
export const flattenObject = <O extends Record<string, T>, T extends O | string | undefined>(
2+
ob: Record<string, T>,
3+
delimiter?: string,
4+
keyTransformer: (key: string) => string = (key) => key,
5+
) => {
26
const result: Record<string, string> = {};
37

48
Object.keys(ob).forEach((i) => {
59
const value = ob[i];
610
if (typeof value === 'object') {
711
const temp = flattenObject(value, delimiter);
812
Object.keys(temp).forEach((j) => {
9-
result[`${i}${delimiter ?? '.'}${j}`] = temp[j];
13+
result[`${keyTransformer(i)}${delimiter ?? '.'}${j}`] = temp[j];
1014
});
1115
} else if (value) {
12-
result[i] = value;
16+
result[keyTransformer(i)] = value;
1317
}
1418
});
1519

1620
return result;
1721
};
22+
23+
export const camelCaseToKebabCase = (value: string) =>
24+
value
25+
.split(/(?=[A-Z])/g)
26+
.join('-')
27+
.toLowerCase();

src/models/style.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,40 @@
1-
import { flattenObject } from '../helper/object';
1+
import { CSSProperties } from 'react';
2+
import { camelCaseToKebabCase, flattenObject } from '../helper/object';
23

34
export type StyleConfig = {
45
line?: {
5-
width?: string;
6-
color?: string;
6+
width?: CSSProperties['width'];
7+
color?: CSSProperties['backgroundColor'];
8+
overhang?: CSSProperties['paddingBlock'];
79
};
8-
gap?: string;
9-
offset?: {
10-
left?: string;
11-
right?: string;
10+
item?: {
11+
gap?: CSSProperties['gap'];
12+
startOffset?: {
13+
left?: CSSProperties['marginTop'];
14+
right?: CSSProperties['marginTop'];
15+
};
1216
};
1317
marker?: {
14-
size?: string;
15-
color?: string;
16-
radius?: string;
18+
size?: CSSProperties['width'];
19+
color?: CSSProperties['backgroundColor'];
20+
radius?: CSSProperties['borderRadius'];
1721
};
1822
pointer?: {
19-
height?: string;
20-
width?: string;
21-
offset?: string;
23+
height?: CSSProperties['height'];
24+
width?: CSSProperties['width'];
25+
minOffset?: CSSProperties['marginTop'];
2226
};
2327
card?: {
24-
background?: string;
25-
radius?: string;
26-
offset?: string;
27-
shadow?: string;
28-
padding?: string;
28+
background?: CSSProperties['backgroundColor'];
29+
radius?: CSSProperties['borderRadius'];
30+
shadow?: CSSProperties['boxShadow'];
31+
padding?: CSSProperties['padding'];
32+
offset?: CSSProperties['gap'];
2933
};
3034
};
3135

3236
export const convertToCssVariable = (styleConfig: StyleConfig) =>
33-
Object.entries(flattenObject(styleConfig, '-')).reduce(
34-
(prev: Record<string, string>, [key, value]) => ({ ...prev, [`--${key}`]: value }),
37+
Object.entries(flattenObject(styleConfig, '-', camelCaseToKebabCase)).reduce(
38+
(prev: Record<string, string>, [key, value]) => ({ ...prev, [`--${key}`]: typeof value === 'number' ? `${value}px` : value }),
3539
{},
3640
);

0 commit comments

Comments
 (0)