From 33c1bfa199d1dbd67609cd840f20ea0998d2ea0c Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Fri, 16 Jun 2023 13:59:30 +0000 Subject: [PATCH 1/8] feat: introduce custom timeline ends --- src/components/Timeline.css | 18 +++++++++++++++++- src/components/Timeline.tsx | 13 +++++++++++-- src/helper/element.ts | 3 +++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/helper/element.ts diff --git a/src/components/Timeline.css b/src/components/Timeline.css index a61dd65..5c224af 100644 --- a/src/components/Timeline.css +++ b/src/components/Timeline.css @@ -23,9 +23,25 @@ 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%; +} + +.timeline-line__end--opening { + transform: translate(-50%, -100%); + top: 0; +} + +.timeline-line__end--closing { + transform: translate(-50%, 100%); + bottom: 0; } .timeline__items-container { diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index 4a34308..46dfc1e 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -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; @@ -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; }; @@ -21,7 +23,7 @@ export const defaultTimelineConfig: Partial = { }; 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, }; @@ -121,7 +123,14 @@ export function Timeline(props: TimelineProps) { ))} -
+
+
+ {isElement(customTimelineEnds) ? customTimelineEnds : customTimelineEnds?.opening} +
+
+ {isElement(customTimelineEnds) ? customTimelineEnds : customTimelineEnds?.closing} +
+
diff --git a/src/helper/element.ts b/src/helper/element.ts new file mode 100644 index 0000000..c50157e --- /dev/null +++ b/src/helper/element.ts @@ -0,0 +1,3 @@ +import { ReactElement, isValidElement } from 'react'; + +export const isElement = (element: any): element is ReactElement => isValidElement(element); From a266d8ed799c66e965d1949579b180f16938968c Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Fri, 16 Jun 2023 14:04:43 +0000 Subject: [PATCH 2/8] docs: examples for custom timeline ends --- src/components/Timeline.stories.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Timeline.stories.tsx b/src/components/Timeline.stories.tsx index b6a269e..00fa540 100644 --- a/src/components/Timeline.stories.tsx +++ b/src/components/Timeline.stories.tsx @@ -257,8 +257,12 @@ customItems[3] = { customPointer:
, }; -export const CustomMarkerAndPointer: StoryObj = { +export const CustomMarkerPointerAndEnds: StoryObj = { args: { + customTimelineEnds: { + opening:
, + closing:
, + }, items: customItems, ...defaultTimelineConfig, customMarker: 🔥, From 74cc33e6dfc6d157930e831fe4cc96a926fd8d3e Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Fri, 16 Jun 2023 14:11:23 +0000 Subject: [PATCH 3/8] chore: update timeline ends styling --- src/components/Timeline.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Timeline.css b/src/components/Timeline.css index 5c224af..46a7ab2 100644 --- a/src/components/Timeline.css +++ b/src/components/Timeline.css @@ -32,15 +32,14 @@ .timeline-line__end { position: absolute; left: 50%; + transform: translate(-50%); } .timeline-line__end--opening { - transform: translate(-50%, -100%); top: 0; } .timeline-line__end--closing { - transform: translate(-50%, 100%); bottom: 0; } From e604de58fcac16d04e9e67d3a341617a99c3fe12 Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Mon, 19 Jun 2023 07:17:47 +0000 Subject: [PATCH 4/8] feat: introduce line overhang configuration --- src/components/Timeline.css | 6 +++--- src/components/Timeline.stories.tsx | 3 +++ src/models/style.ts | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Timeline.css b/src/components/Timeline.css index 46a7ab2..1cc7a37 100644 --- a/src/components/Timeline.css +++ b/src/components/Timeline.css @@ -1,6 +1,7 @@ :where(.timeline) { --line-width: 0.2rem; --line-color: black; + --line-overhang: 1rem; --marker-size: 1rem; --marker-color: var(--line-color); --marker-radius: 50%; @@ -12,11 +13,9 @@ --card-offset: 1rem; --card-shadow: unset; --card-padding: 1rem; - --gap: 1rem; --offset-left: 0; --offset-right: 5rem; - - position: relative; + --gap: 1rem; } .timeline { @@ -45,6 +44,7 @@ .timeline__items-container { display: flex; + padding-block: var(--line-overhang); flex-direction: column; gap: var(--gap); flex: 1; diff --git a/src/components/Timeline.stories.tsx b/src/components/Timeline.stories.tsx index 00fa540..97a4308 100644 --- a/src/components/Timeline.stories.tsx +++ b/src/components/Timeline.stories.tsx @@ -169,6 +169,9 @@ export const SpacingOptions: StoryObj = { pointer: { offset: '4rem', }, + line: { + overhang: '4rem', + }, }, minMarkerGap: 150, }, diff --git a/src/models/style.ts b/src/models/style.ts index 476f37f..9425c24 100644 --- a/src/models/style.ts +++ b/src/models/style.ts @@ -4,6 +4,7 @@ export type StyleConfig = { line?: { width?: string; color?: string; + overhang?: string; }; gap?: string; offset?: { From a054e68b36d80334998d0a6902e0b1c9f53440ca Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Mon, 19 Jun 2023 10:53:24 +0000 Subject: [PATCH 5/8] refactor: rename and restructure styling variables --- src/components/Timeline.css | 20 ++++++------- src/components/Timeline.stories.tsx | 14 +++++---- src/helper/object.ts | 16 ++++++++-- src/models/style.ts | 45 +++++++++++++++-------------- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/components/Timeline.css b/src/components/Timeline.css index 1cc7a37..6df3c72 100644 --- a/src/components/Timeline.css +++ b/src/components/Timeline.css @@ -2,20 +2,20 @@ --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; + --pointer-spacing: 1rem; --card-background: whitesmoke; --card-radius: 0.1rem; - --card-offset: 1rem; --card-shadow: unset; --card-padding: 1rem; - --offset-left: 0; - --offset-right: 5rem; - --gap: 1rem; } .timeline { @@ -46,16 +46,16 @@ display: flex; padding-block: var(--line-overhang); flex-direction: column; - gap: var(--gap); + gap: var(--item-gap); flex: 1; } .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 { @@ -63,7 +63,7 @@ display: flex; align-items: flex-start; transition: top 0.1s; - gap: var(--card-offset); + gap: var(--pointer-spacing); } .timeline__items-container--left .timeline-item { @@ -71,7 +71,7 @@ } .timeline-item__marker { - margin-top: var(--pointer-offset); + margin-top: var(--pointer-min-offset); } .timeline-item__marker:not(.timeline-item__marker--custom) { diff --git a/src/components/Timeline.stories.tsx b/src/components/Timeline.stories.tsx index 97a4308..f6528b2 100644 --- a/src/components/Timeline.stories.tsx +++ b/src/components/Timeline.stories.tsx @@ -161,13 +161,15 @@ export const SpacingOptions: StoryObj = { 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', @@ -191,7 +193,6 @@ export const CustomStyle: StoryObj = { background: 'white', radius: '.2rem', shadow: '.1rem .1rem .5rem rgb(0,0,0,0.1)', - offset: '2rem', }, line: { width: '.1rem', @@ -205,6 +206,7 @@ export const CustomStyle: StoryObj = { pointer: { width: '2rem', height: '2rem', + spacing: '2rem', }, }, }, diff --git a/src/helper/object.ts b/src/helper/object.ts index 90529fd..d9765e6 100644 --- a/src/helper/object.ts +++ b/src/helper/object.ts @@ -1,4 +1,8 @@ -export const flattenObject = , T extends O | string | undefined>(ob: Record, delimiter?: string) => { +export const flattenObject = , T extends O | string | undefined>( + ob: Record, + delimiter?: string, + keyTransformer: (key: string) => string = (key) => key, +) => { const result: Record = {}; Object.keys(ob).forEach((i) => { @@ -6,12 +10,18 @@ export const flattenObject = , T extends O | string 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(); diff --git a/src/models/style.ts b/src/models/style.ts index 9425c24..cd459ce 100644 --- a/src/models/style.ts +++ b/src/models/style.ts @@ -1,37 +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; - overhang?: 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']; + spacing?: CSSProperties['gap']; }; card?: { - background?: string; - radius?: string; - offset?: string; - shadow?: string; - padding?: string; + background?: CSSProperties['backgroundColor']; + radius?: CSSProperties['borderRadius']; + shadow?: CSSProperties['boxShadow']; + padding?: CSSProperties['padding']; }; }; export const convertToCssVariable = (styleConfig: StyleConfig) => - Object.entries(flattenObject(styleConfig, '-')).reduce( - (prev: Record, [key, value]) => ({ ...prev, [`--${key}`]: value }), + Object.entries(flattenObject(styleConfig, '-', camelCaseToKebabCase)).reduce( + (prev: Record, [key, value]) => ({ ...prev, [`--${key}`]: typeof value === 'number' ? `${value}px` : value }), {}, ); From 6a3090f35eefc43b90f3f84921d24f753a932b0b Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Wed, 21 Jun 2023 07:12:15 +0000 Subject: [PATCH 6/8] refactor: rename style variable --- src/components/Timeline.css | 4 ++-- src/models/style.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Timeline.css b/src/components/Timeline.css index 6df3c72..f4c9d0a 100644 --- a/src/components/Timeline.css +++ b/src/components/Timeline.css @@ -11,11 +11,11 @@ --pointer-height: 2rem; --pointer-width: 1rem; --pointer-min-offset: 5rem; - --pointer-spacing: 1rem; --card-background: whitesmoke; --card-radius: 0.1rem; --card-shadow: unset; --card-padding: 1rem; + --card-offset: 1rem; } .timeline { @@ -63,7 +63,7 @@ display: flex; align-items: flex-start; transition: top 0.1s; - gap: var(--pointer-spacing); + gap: var(--card-offset); } .timeline__items-container--left .timeline-item { diff --git a/src/models/style.ts b/src/models/style.ts index cd459ce..4637981 100644 --- a/src/models/style.ts +++ b/src/models/style.ts @@ -23,13 +23,13 @@ export type StyleConfig = { height?: CSSProperties['height']; width?: CSSProperties['width']; minOffset?: CSSProperties['marginTop']; - spacing?: CSSProperties['gap']; }; card?: { background?: CSSProperties['backgroundColor']; radius?: CSSProperties['borderRadius']; shadow?: CSSProperties['boxShadow']; padding?: CSSProperties['padding']; + offset?: CSSProperties['gap']; }; }; From 69f5c54de0760936606fca555cc5c8192751f4d1 Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Wed, 21 Jun 2023 07:13:39 +0000 Subject: [PATCH 7/8] docs: enhance styling documentation --- README.md | 87 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9003108..8eb7721 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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 From c4b92d27317acf15c83a33726c1666bb903be486 Mon Sep 17 00:00:00 2001 From: stampaaaron Date: Wed, 21 Jun 2023 07:14:15 +0000 Subject: [PATCH 8/8] fix: timeline example --- src/components/Timeline.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Timeline.stories.tsx b/src/components/Timeline.stories.tsx index f6528b2..141c2e7 100644 --- a/src/components/Timeline.stories.tsx +++ b/src/components/Timeline.stories.tsx @@ -193,6 +193,7 @@ export const CustomStyle: StoryObj = { background: 'white', radius: '.2rem', shadow: '.1rem .1rem .5rem rgb(0,0,0,0.1)', + offset: '2rem', }, line: { width: '.1rem', @@ -206,7 +207,6 @@ export const CustomStyle: StoryObj = { pointer: { width: '2rem', height: '2rem', - spacing: '2rem', }, }, },