Skip to content

82 add soft timeline ends #109

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 58 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# react-alternating-timeline

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

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

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

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

### StyleConfig
## Styling

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

```ts
{
line?: {
width?: string;
color?: string;
width?: CSSProperties['width'];
color?: CSSProperties['backgroundColor'];
overhang?: CSSProperties['paddingBlock'];
};
gap?: string;
offset?: {
left?: string;
right?: string;
item?: {
gap?: CSSProperties['gap'];
startOffset?: {
left?: CSSProperties['marginTop'];
right?: CSSProperties['marginTop'];
};
};
marker?: {
size?: string;
color?: string;
radius?: string;
size?: CSSProperties['width'];
color?: CSSProperties['backgroundColor'];
radius?: CSSProperties['borderRadius'];
};
pointer?: {
height?: string;
width?: string;
offset?: string;
height?: CSSProperties['height'];
width?: CSSProperties['width'];
minOffset?: CSSProperties['marginTop'];
};
card?: {
background?: string;
radius?: string;
offset?: string;
shadow?: string;
padding?: string;
background?: CSSProperties['backgroundColor'];
radius?: CSSProperties['borderRadius'];
shadow?: CSSProperties['boxShadow'];
padding?: CSSProperties['padding'];
offset?: CSSProperties['gap'];
};
};
}
```

...or the custom properties can be set directly in css
...or can be set as custom properties directly in css

```css
.timeline {
--line-width: 0.2rem;
--line-color: black;
--line-overhang: 1rem;
--item-gap: 1rem;
--item-start-offset-left: 0;
--item-start-offset-right: 5rem;
--marker-size: 1rem;
--marker-color: var(--line-color);
--marker-radius: 50%;
--pointer-height: 2rem;
--pointer-width: 1rem;
--pointer-offset: 5rem;
--pointer-min-offset: 5rem;
--card-background: whitesmoke;
--card-radius: 0.1rem;
--card-offset: 1rem;
--card-shadow: unset;
--card-padding: 1rem;
--gap: 1rem;
--offset-left: 0;
--offset-right: 5rem;
--card-offset: 1rem;
}
```

(These are the default values)
### StyleConfig

| Name | Description | Default |
| :------------------------- | :------------------------------------------------------------------------------------------------------------- | :------------ |
| **Line** | The line the timeline items are place around/beside | |
| – `line-width` | Width of the line | `0.2rem` |
| – `line-color` | Color of the line | `black` |
| – `line-overhang` | How much the line should overhang the beginning and end of the timeline component | `1rem` |
| **Item** | The timeline item as a whole, including the card, pointer and marker | |
| – `item-gap` | The vertical space between the items | `1rem` |
| – `item-start-offset-left` | How much the items on the left side should be offset from the top | `0` |
| – `item-start-offset-left` | How much the items on the right side should be offset from the top | `5rem` |
| **Marker** | The markers on the line which marks the chronological order of the timeline items | `1rem` |
| – `marker-size` | Size of the default marker | `1rem` |
| – `marker-color` | Color of the default marker | `line-color` |
| – `marker-radius` | Border radius (roundness) of the marker edges | `50%` (round) |
| **Pointer** | The pointers pointing from the item cards to the markers on the line | |
| – `pointer-height` | Height of the default pointer | `2rem` |
| – `pointer-width` | Width of the default pointer | `1rem` |
| – `pointer-min-offset` | Minimum offset of the pointer to the top of the card. The actual offset depends on the `minMarkerGap` property | `5rem` |
| **Card** | The cards in which the timeline item content is displayed | |
| – `card-background` | Background color of the card | `whitesmoke` |
| – `card-radius` | Border radius of the card edges | `0.1rem` |
| – `card-shadow` | Configure drop shadow of the card | `unset` |
| – `card-padding` | Padding of the card content | `1rem` |
| – `card-offset` | Space between the card and the timeline line | `1rem` |

## Demo

Expand Down
38 changes: 27 additions & 11 deletions src/components/Timeline.css
Original file line number Diff line number Diff line change
@@ -1,45 +1,61 @@
:where(.timeline) {
--line-width: 0.2rem;
--line-color: black;
--line-overhang: 1rem;
--item-gap: 1rem;
--item-start-offset-left: 0;
--item-start-offset-right: 5rem;
--marker-size: 1rem;
--marker-color: var(--line-color);
--marker-radius: 50%;
--pointer-height: 2rem;
--pointer-width: 1rem;
--pointer-offset: 5rem;
--pointer-min-offset: 5rem;
--card-background: whitesmoke;
--card-radius: 0.1rem;
--card-offset: 1rem;
--card-shadow: unset;
--card-padding: 1rem;
--gap: 1rem;
--offset-left: 0;
--offset-right: 5rem;

position: relative;
--card-offset: 1rem;
}

.timeline {
display: flex;
}

.timeline__line {
.timeline-line {
background-color: var(--line-color);
min-width: var(--line-width);
position: relative;
}

.timeline-line__end {
position: absolute;
left: 50%;
transform: translate(-50%);
}

.timeline-line__end--opening {
top: 0;
}

.timeline-line__end--closing {
bottom: 0;
}

.timeline__items-container {
display: flex;
padding-block: var(--line-overhang);
flex-direction: column;
flex: 1;
gap: var(--item-gap);
}

.timeline__items-container--left {
margin-top: var(--offset-left);
margin-top: var(--item-start-offset-left);
}

.timeline__items-container--right {
margin-top: var(--offset-right);
margin-top: var(--item-start-offset-right);
}

.timeline-item {
Expand All @@ -59,7 +75,7 @@
}

.timeline-item__marker {
margin-top: var(--pointer-offset);
margin-top: var(--pointer-min-offset);
}

.timeline-item__marker:not(.timeline-item__marker--custom) {
Expand Down
21 changes: 15 additions & 6 deletions src/components/Timeline.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,18 @@ export const SpacingOptions: StoryObj<typeof Timeline> = {
items,
...defaultTimelineConfig,
styleConfig: {
gap: '5rem',
offset: {
left: '10rem',
right: '3rem',
item: {
gap: '5rem',
startOffset: {
left: '10rem',
right: '3rem',
},
},
pointer: {
offset: '4rem',
minOffset: '4rem',
},
line: {
overhang: '4rem',
},
},
minMarkerGap: 150,
Expand Down Expand Up @@ -257,8 +262,12 @@ customItems[3] = {
customPointer: <div className="pointy" style={{ backgroundColor: 'green' }} />,
};

export const CustomMarkerAndPointer: StoryObj<typeof Timeline> = {
export const CustomMarkerPointerAndEnds: StoryObj<typeof Timeline> = {
args: {
customTimelineEnds: {
opening: <div style={{ width: '1rem', height: '1rem', backgroundColor: 'green' }} />,
closing: <div style={{ width: '1rem', height: '1rem', backgroundColor: 'blue' }} />,
},
items: customItems,
...defaultTimelineConfig,
customMarker: <span style={{ fontSize: '2rem' }}>🔥</span>,
Expand Down
13 changes: 11 additions & 2 deletions src/components/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Key, ReactElement, useEffect, useRef } from 'react';
import { TimelineItem, TimelineItemRefs, TimelineItemsProps } from './TimelineItem';
import { Positioning } from '../models/positioning';
import { convertToCssVariable, StyleConfig } from '../models/style';
import { isElement } from '../helper/element';

export type TimelineProps = {
items: TimelineItemsProps;
Expand All @@ -11,6 +12,7 @@ export type TimelineProps = {
formatDate?: (date: Date) => string;
customMarker?: ReactElement;
customPointer?: ReactElement;
customTimelineEnds?: ReactElement | { opening?: ReactElement; closing?: ReactElement };
styleConfig?: StyleConfig;
className?: string;
};
Expand All @@ -21,7 +23,7 @@ export const defaultTimelineConfig: Partial<TimelineProps> = {
};

export function Timeline(props: TimelineProps) {
const { items, positioning, minMarkerGap, className, customMarker, customPointer, styleConfig, formatDate } = {
const { items, positioning, minMarkerGap, className, customMarker, customPointer, customTimelineEnds, styleConfig, formatDate } = {
...defaultTimelineConfig,
...props,
};
Expand Down Expand Up @@ -128,7 +130,14 @@ export function Timeline(props: TimelineProps) {
))}
</div>

<div className="timeline__line" />
<div className="timeline-line">
<div className="timeline-line__end timeline-line__end--opening">
{isElement(customTimelineEnds) ? customTimelineEnds : customTimelineEnds?.opening}
</div>
<div className="timeline-line__end timeline-line__end--closing">
{isElement(customTimelineEnds) ? customTimelineEnds : customTimelineEnds?.closing}
</div>
</div>

<div ref={rightContainer} className="timeline__items-container timeline__items-container--right" />
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/helper/element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { ReactElement, isValidElement } from 'react';

export const isElement = (element: any): element is ReactElement => isValidElement(element);
16 changes: 13 additions & 3 deletions src/helper/object.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
export const flattenObject = <O extends Record<string, T>, T extends O | string | undefined>(ob: Record<string, T>, delimiter?: string) => {
export const flattenObject = <O extends Record<string, T>, T extends O | string | undefined>(
ob: Record<string, T>,
delimiter?: string,
keyTransformer: (key: string) => string = (key) => key,
) => {
const result: Record<string, string> = {};

Object.keys(ob).forEach((i) => {
const value = ob[i];
if (typeof value === 'object') {
const temp = flattenObject(value, delimiter);
Object.keys(temp).forEach((j) => {
result[`${i}${delimiter ?? '.'}${j}`] = temp[j];
result[`${keyTransformer(i)}${delimiter ?? '.'}${j}`] = temp[j];
});
} else if (value) {
result[i] = value;
result[keyTransformer(i)] = value;
}
});

return result;
};

export const camelCaseToKebabCase = (value: string) =>
value
.split(/(?=[A-Z])/g)
.join('-')
.toLowerCase();
44 changes: 24 additions & 20 deletions src/models/style.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
import { flattenObject } from '../helper/object';
import { CSSProperties } from 'react';
import { camelCaseToKebabCase, flattenObject } from '../helper/object';

export type StyleConfig = {
line?: {
width?: string;
color?: string;
width?: CSSProperties['width'];
color?: CSSProperties['backgroundColor'];
overhang?: CSSProperties['paddingBlock'];
};
gap?: string;
offset?: {
left?: string;
right?: string;
item?: {
gap?: CSSProperties['gap'];
startOffset?: {
left?: CSSProperties['marginTop'];
right?: CSSProperties['marginTop'];
};
};
marker?: {
size?: string;
color?: string;
radius?: string;
size?: CSSProperties['width'];
color?: CSSProperties['backgroundColor'];
radius?: CSSProperties['borderRadius'];
};
pointer?: {
height?: string;
width?: string;
offset?: string;
height?: CSSProperties['height'];
width?: CSSProperties['width'];
minOffset?: CSSProperties['marginTop'];
};
card?: {
background?: string;
radius?: string;
offset?: string;
shadow?: string;
padding?: string;
background?: CSSProperties['backgroundColor'];
radius?: CSSProperties['borderRadius'];
shadow?: CSSProperties['boxShadow'];
padding?: CSSProperties['padding'];
offset?: CSSProperties['gap'];
};
};

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