├── .all-contributorsrc ├── .dccache ├── .deepsource.toml ├── .depcheckrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── coveralls.yml │ ├── cypress.yml │ ├── snyk.yaml │ └── sonar.yml ├── .gitignore ├── .kodiak.toml ├── .prettierignore ├── .prettierrc ├── .restyled.yaml ├── .snyk ├── .stylelintrc ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DARK_MODE_IMPROVEMENTS.md ├── LICENSE ├── PERFORMANCE_IMPROVEMENTS.md ├── README.md ├── babel.config.js ├── browserstack.json ├── coverage ├── base.css ├── block-navigation.js ├── clover.xml ├── components │ ├── common │ │ ├── styles │ │ │ ├── index.html │ │ │ └── index.ts.html │ │ └── themes │ │ │ ├── index.html │ │ │ └── index.ts.html │ ├── effects │ │ ├── __tests__ │ │ │ ├── index.html │ │ │ ├── useClickOutside.test.tsx.html │ │ │ ├── useMatchMedia.test.tsx.html │ │ │ └── useSlideshow.test.ts.html │ │ ├── index.html │ │ ├── useCloseClickOutside.ts.html │ │ ├── useMatchMedia.ts.html │ │ ├── useNewScrollPosition.ts.html │ │ └── useSlideshow.ts.html │ ├── elements │ │ ├── list │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ ├── list-item.test.tsx.html │ │ │ │ └── list.test.tsx.html │ │ │ ├── index.html │ │ │ ├── list-item.tsx.html │ │ │ ├── list.model.ts.html │ │ │ ├── list.styles.ts.html │ │ │ └── list.tsx.html │ │ └── popover │ │ │ ├── __tests__ │ │ │ ├── index.html │ │ │ └── popover.test.tsx.html │ │ │ ├── index.html │ │ │ ├── index.tsx.html │ │ │ ├── popover.model.ts.html │ │ │ └── popover.styles.ts.html │ ├── timeline-elements │ │ ├── memoized │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ └── index.test.tsx.html │ │ │ ├── details-text-memo.tsx.html │ │ │ ├── expand-button-memo.tsx.html │ │ │ ├── index.html │ │ │ ├── memoized-model.ts.html │ │ │ ├── show-hide-button.tsx.html │ │ │ ├── subtitle-memo.tsx.html │ │ │ └── title-memo.tsx.html │ │ ├── timeline-card-content │ │ │ ├── __tests__ │ │ │ │ ├── content-footer.test.tsx.html │ │ │ │ ├── content-header.test.tsx.html │ │ │ │ ├── details-text.test.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── text_or_content.test.tsx.html │ │ │ │ └── timeline-card-content.test.tsx.html │ │ │ ├── card-animations.styles.ts.html │ │ │ ├── content-footer.tsx.html │ │ │ ├── content-header.tsx.html │ │ │ ├── details-text.model.ts.html │ │ │ ├── details-text.tsx.html │ │ │ ├── header-footer.model.ts.html │ │ │ ├── index.html │ │ │ ├── text-or-content.tsx.html │ │ │ ├── timeline-card-content.styles.ts.html │ │ │ └── timeline-card-content.tsx.html │ │ ├── timeline-card-media │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ └── timeline-card-media.test.tsx.html │ │ │ ├── components │ │ │ │ ├── ContentDisplay.tsx.html │ │ │ │ ├── ErrorMessage.tsx.html │ │ │ │ ├── ImageDisplay.tsx.html │ │ │ │ ├── MediaContent.tsx.html │ │ │ │ ├── VideoPlayer.tsx.html │ │ │ │ ├── YoutubePlayer.tsx.html │ │ │ │ └── index.html │ │ │ ├── hooks │ │ │ │ ├── index.html │ │ │ │ ├── useMediaLoad.ts.html │ │ │ │ ├── useToggleControls.ts.html │ │ │ │ ├── useViewOptions.ts.html │ │ │ │ └── useYouTubeDetection.ts.html │ │ │ ├── index.html │ │ │ ├── timeline-card-media-buttons.tsx.html │ │ │ ├── timeline-card-media.styles.ts.html │ │ │ ├── timeline-card-media.tsx.html │ │ │ └── video.tsx.html │ │ ├── timeline-card │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ └── timeline-horizontal-card.test.tsx.html │ │ │ ├── hooks │ │ │ │ ├── index.html │ │ │ │ └── useTimelineCard.ts.html │ │ │ ├── index.html │ │ │ ├── timeline-card-portal │ │ │ │ ├── index.html │ │ │ │ └── timeline-card-portal.tsx.html │ │ │ ├── timeline-horizontal-card.styles.ts.html │ │ │ ├── timeline-horizontal-card.tsx.html │ │ │ └── timeline-point │ │ │ │ ├── index.html │ │ │ │ └── timeline-point.tsx.html │ │ ├── timeline-control │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ └── timeline-control.test.tsx.html │ │ │ ├── index.html │ │ │ ├── timeline-control.styles.ts.html │ │ │ ├── timeline-control.styles.tsx.html │ │ │ └── timeline-control.tsx.html │ │ ├── timeline-item-title │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ └── timeline-card-title.test.tsx.html │ │ │ ├── index.html │ │ │ ├── timeline-card-title.styles.ts.html │ │ │ └── timeline-card-title.tsx.html │ │ └── timeline-outline │ │ │ ├── __tests__ │ │ │ ├── index.html │ │ │ └── timeline-outline.test.tsx.html │ │ │ ├── animations.ts.html │ │ │ ├── hooks │ │ │ ├── index.html │ │ │ └── useOutlinePosition.ts.html │ │ │ ├── index.html │ │ │ ├── timeline-outline-item-list.tsx.html │ │ │ ├── timeline-outline.model.ts.html │ │ │ ├── timeline-outline.styles.ts.html │ │ │ └── timeline-outline.tsx.html │ ├── timeline-horizontal │ │ ├── index.html │ │ ├── timeline-horizontal.styles.ts.html │ │ └── timeline-horizontal.tsx.html │ ├── timeline-vertical │ │ ├── __tests__ │ │ │ ├── index.html │ │ │ ├── timeline-point.test.tsx.html │ │ │ └── timeline-vertical-item.test.tsx.html │ │ ├── index.html │ │ ├── timeline-point.tsx.html │ │ ├── timeline-vertical-item.tsx.html │ │ ├── timeline-vertical-shape.styles.ts.html │ │ ├── timeline-vertical.styles.ts.html │ │ └── timeline-vertical.tsx.html │ ├── timeline │ │ ├── TimelineView.tsx.html │ │ ├── __tests__ │ │ │ ├── index.html │ │ │ ├── layout-switcher.test.tsx.html │ │ │ └── timeline.test.tsx.html │ │ ├── index.html │ │ ├── timeline-popover-elements.tsx.html │ │ ├── timeline-popover.model.ts.html │ │ ├── timeline-toolbar.model.ts.html │ │ ├── timeline-toolbar.tsx.html │ │ ├── timeline.style.ts.html │ │ ├── timeline.style.tsx.html │ │ └── timeline.tsx.html │ ├── toggle-button │ │ ├── index.html │ │ ├── index.tsx.html │ │ └── toggle-button.styles.ts.html │ └── toolbar │ │ ├── __tests__ │ │ ├── index.html │ │ └── toolbar.test.tsx.html │ │ ├── index.html │ │ ├── index.tsx.html │ │ └── toolbar.styles.ts.html ├── favicon.png ├── hooks │ ├── __tests__ │ │ ├── index.html │ │ ├── useBackground.test.ts.html │ │ ├── useCardSize.test.ts.html │ │ ├── useEscapeKey.test.ts.html │ │ ├── useMediaState.test.ts.html │ │ ├── useOutsideClick.test.ts.html │ │ ├── useTimelineMedia.test.ts.html │ │ ├── useTimelineMode.test.ts.html │ │ ├── useTimelineNavigation.test.ts.html │ │ ├── useTimelineScroll.test.ts.html │ │ ├── useTimelineSearch.test.ts.html │ │ ├── useUIState.test.ts.html │ │ └── useWindowSize.test.ts.html │ ├── index.html │ ├── useBackground.ts.html │ ├── useCardSize.ts.html │ ├── useEscapeKey.ts.html │ ├── useMediaState.ts.html │ ├── useOutsideClick.ts.html │ ├── useTimelineMedia.ts.html │ ├── useTimelineMode.ts.html │ ├── useTimelineNavigation.ts.html │ ├── useTimelineScroll.ts.html │ ├── useTimelineSearch.ts.html │ ├── useUIState.ts.html │ └── useWindowSize.ts.html ├── index.html ├── lcov-report │ ├── base.css │ ├── block-navigation.js │ ├── components │ │ ├── common │ │ │ ├── styles │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ │ └── themes │ │ │ │ ├── index.html │ │ │ │ └── index.ts.html │ │ ├── effects │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ ├── useClickOutside.test.tsx.html │ │ │ │ ├── useMatchMedia.test.tsx.html │ │ │ │ └── useSlideshow.test.ts.html │ │ │ ├── index.html │ │ │ ├── useCloseClickOutside.ts.html │ │ │ ├── useMatchMedia.ts.html │ │ │ ├── useNewScrollPosition.ts.html │ │ │ └── useSlideshow.ts.html │ │ ├── elements │ │ │ ├── list │ │ │ │ ├── __tests__ │ │ │ │ │ ├── index.html │ │ │ │ │ ├── list-item.test.tsx.html │ │ │ │ │ └── list.test.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── list-item.tsx.html │ │ │ │ ├── list.model.ts.html │ │ │ │ ├── list.styles.ts.html │ │ │ │ └── list.tsx.html │ │ │ └── popover │ │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ └── popover.test.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── index.tsx.html │ │ │ │ ├── popover.model.ts.html │ │ │ │ └── popover.styles.ts.html │ │ ├── timeline-elements │ │ │ ├── memoized │ │ │ │ ├── __tests__ │ │ │ │ │ ├── index.html │ │ │ │ │ └── index.test.tsx.html │ │ │ │ ├── details-text-memo.tsx.html │ │ │ │ ├── expand-button-memo.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── memoized-model.ts.html │ │ │ │ ├── show-hide-button.tsx.html │ │ │ │ ├── subtitle-memo.tsx.html │ │ │ │ └── title-memo.tsx.html │ │ │ ├── timeline-card-content │ │ │ │ ├── __tests__ │ │ │ │ │ ├── content-footer.test.tsx.html │ │ │ │ │ ├── content-header.test.tsx.html │ │ │ │ │ ├── details-text.test.tsx.html │ │ │ │ │ ├── index.html │ │ │ │ │ ├── text_or_content.test.tsx.html │ │ │ │ │ └── timeline-card-content.test.tsx.html │ │ │ │ ├── card-animations.styles.ts.html │ │ │ │ ├── content-footer.tsx.html │ │ │ │ ├── content-header.tsx.html │ │ │ │ ├── details-text.model.ts.html │ │ │ │ ├── details-text.tsx.html │ │ │ │ ├── header-footer.model.ts.html │ │ │ │ ├── index.html │ │ │ │ ├── text-or-content.tsx.html │ │ │ │ ├── timeline-card-content.styles.ts.html │ │ │ │ └── timeline-card-content.tsx.html │ │ │ ├── timeline-card-media │ │ │ │ ├── __tests__ │ │ │ │ │ ├── index.html │ │ │ │ │ └── timeline-card-media.test.tsx.html │ │ │ │ ├── components │ │ │ │ │ ├── ContentDisplay.tsx.html │ │ │ │ │ ├── ErrorMessage.tsx.html │ │ │ │ │ ├── ImageDisplay.tsx.html │ │ │ │ │ ├── MediaContent.tsx.html │ │ │ │ │ ├── VideoPlayer.tsx.html │ │ │ │ │ ├── YoutubePlayer.tsx.html │ │ │ │ │ └── index.html │ │ │ │ ├── hooks │ │ │ │ │ ├── index.html │ │ │ │ │ ├── useMediaLoad.ts.html │ │ │ │ │ ├── useToggleControls.ts.html │ │ │ │ │ ├── useViewOptions.ts.html │ │ │ │ │ └── useYouTubeDetection.ts.html │ │ │ │ ├── index.html │ │ │ │ ├── timeline-card-media-buttons.tsx.html │ │ │ │ ├── timeline-card-media.styles.ts.html │ │ │ │ ├── timeline-card-media.tsx.html │ │ │ │ └── video.tsx.html │ │ │ ├── timeline-card │ │ │ │ ├── __tests__ │ │ │ │ │ ├── index.html │ │ │ │ │ └── timeline-horizontal-card.test.tsx.html │ │ │ │ ├── hooks │ │ │ │ │ ├── index.html │ │ │ │ │ └── useTimelineCard.ts.html │ │ │ │ ├── index.html │ │ │ │ ├── timeline-card-portal │ │ │ │ │ ├── index.html │ │ │ │ │ └── timeline-card-portal.tsx.html │ │ │ │ ├── timeline-horizontal-card.styles.ts.html │ │ │ │ ├── timeline-horizontal-card.tsx.html │ │ │ │ └── timeline-point │ │ │ │ │ ├── index.html │ │ │ │ │ └── timeline-point.tsx.html │ │ │ ├── timeline-control │ │ │ │ ├── __tests__ │ │ │ │ │ ├── index.html │ │ │ │ │ └── timeline-control.test.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── timeline-control.styles.ts.html │ │ │ │ ├── timeline-control.styles.tsx.html │ │ │ │ └── timeline-control.tsx.html │ │ │ ├── timeline-item-title │ │ │ │ ├── __tests__ │ │ │ │ │ ├── index.html │ │ │ │ │ └── timeline-card-title.test.tsx.html │ │ │ │ ├── index.html │ │ │ │ ├── timeline-card-title.styles.ts.html │ │ │ │ └── timeline-card-title.tsx.html │ │ │ └── timeline-outline │ │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ └── timeline-outline.test.tsx.html │ │ │ │ ├── animations.ts.html │ │ │ │ ├── hooks │ │ │ │ ├── index.html │ │ │ │ └── useOutlinePosition.ts.html │ │ │ │ ├── index.html │ │ │ │ ├── timeline-outline-item-list.tsx.html │ │ │ │ ├── timeline-outline.model.ts.html │ │ │ │ ├── timeline-outline.styles.ts.html │ │ │ │ └── timeline-outline.tsx.html │ │ ├── timeline-horizontal │ │ │ ├── index.html │ │ │ ├── timeline-horizontal.styles.ts.html │ │ │ └── timeline-horizontal.tsx.html │ │ ├── timeline-vertical │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ ├── timeline-point.test.tsx.html │ │ │ │ └── timeline-vertical-item.test.tsx.html │ │ │ ├── index.html │ │ │ ├── timeline-point.tsx.html │ │ │ ├── timeline-vertical-item.tsx.html │ │ │ ├── timeline-vertical-shape.styles.ts.html │ │ │ ├── timeline-vertical.styles.ts.html │ │ │ └── timeline-vertical.tsx.html │ │ ├── timeline │ │ │ ├── TimelineView.tsx.html │ │ │ ├── __tests__ │ │ │ │ ├── index.html │ │ │ │ ├── layout-switcher.test.tsx.html │ │ │ │ └── timeline.test.tsx.html │ │ │ ├── index.html │ │ │ ├── timeline-popover-elements.tsx.html │ │ │ ├── timeline-popover.model.ts.html │ │ │ ├── timeline-toolbar.model.ts.html │ │ │ ├── timeline-toolbar.tsx.html │ │ │ ├── timeline.style.ts.html │ │ │ ├── timeline.style.tsx.html │ │ │ └── timeline.tsx.html │ │ ├── toggle-button │ │ │ ├── index.html │ │ │ ├── index.tsx.html │ │ │ └── toggle-button.styles.ts.html │ │ └── toolbar │ │ │ ├── __tests__ │ │ │ ├── index.html │ │ │ └── toolbar.test.tsx.html │ │ │ ├── index.html │ │ │ ├── index.tsx.html │ │ │ └── toolbar.styles.ts.html │ ├── favicon.png │ ├── hooks │ │ ├── __tests__ │ │ │ ├── index.html │ │ │ ├── useBackground.test.ts.html │ │ │ ├── useCardSize.test.ts.html │ │ │ ├── useEscapeKey.test.ts.html │ │ │ ├── useMediaState.test.ts.html │ │ │ ├── useOutsideClick.test.ts.html │ │ │ ├── useTimelineMedia.test.ts.html │ │ │ ├── useTimelineMode.test.ts.html │ │ │ ├── useTimelineNavigation.test.ts.html │ │ │ ├── useTimelineScroll.test.ts.html │ │ │ ├── useTimelineSearch.test.ts.html │ │ │ ├── useUIState.test.ts.html │ │ │ └── useWindowSize.test.ts.html │ │ ├── index.html │ │ ├── useBackground.ts.html │ │ ├── useCardSize.ts.html │ │ ├── useEscapeKey.ts.html │ │ ├── useMediaState.ts.html │ │ ├── useOutsideClick.ts.html │ │ ├── useTimelineMedia.ts.html │ │ ├── useTimelineMode.ts.html │ │ ├── useTimelineNavigation.ts.html │ │ ├── useTimelineScroll.ts.html │ │ ├── useTimelineSearch.ts.html │ │ ├── useUIState.ts.html │ │ └── useWindowSize.ts.html │ ├── index.html │ ├── prettify.css │ ├── prettify.js │ ├── react-chrono │ │ └── src │ │ │ └── hooks │ │ │ └── __tests__ │ │ │ ├── index.html │ │ │ ├── useBackground.test.ts.html │ │ │ ├── useCardSize.test.ts.html │ │ │ ├── useEscapeKey.test.ts.html │ │ │ ├── useMediaState.test.ts.html │ │ │ ├── useOutsideClick.test.ts.html │ │ │ ├── useTimelineMedia.test.ts.html │ │ │ ├── useTimelineMode.test.ts.html │ │ │ ├── useTimelineNavigation.test.ts.html │ │ │ ├── useTimelineScroll.test.ts.html │ │ │ ├── useTimelineSearch.test.ts.html │ │ │ ├── useUIState.test.ts.html │ │ │ └── useWindowSize.test.ts.html │ ├── sort-arrow-sprite.png │ ├── sorter.js │ └── utils │ │ ├── index.html │ │ ├── index.ts.html │ │ ├── mediaQueryUtils.ts.html │ │ ├── timelineUtils.ts.html │ │ └── utils.test.ts.html ├── lcov.info ├── prettify.css ├── prettify.js ├── react-chrono │ └── src │ │ └── hooks │ │ └── __tests__ │ │ ├── index.html │ │ ├── useBackground.test.ts.html │ │ ├── useCardSize.test.ts.html │ │ ├── useEscapeKey.test.ts.html │ │ ├── useMediaState.test.ts.html │ │ ├── useOutsideClick.test.ts.html │ │ ├── useTimelineMedia.test.ts.html │ │ ├── useTimelineMode.test.ts.html │ │ ├── useTimelineNavigation.test.ts.html │ │ ├── useTimelineScroll.test.ts.html │ │ ├── useTimelineSearch.test.ts.html │ │ ├── useUIState.test.ts.html │ │ └── useWindowSize.test.ts.html ├── sort-arrow-sprite.png ├── sorter.js └── utils │ ├── index.html │ ├── index.ts.html │ ├── mediaQueryUtils.ts.html │ ├── timelineUtils.ts.html │ └── utils.test.ts.html ├── cypress.config.ts ├── cypress ├── e2e │ ├── examples │ │ ├── actions.spec.js │ │ ├── aliasing.spec.js │ │ ├── assertions.spec.js │ │ ├── connectors.spec.js │ │ ├── cookies.spec.js │ │ ├── cypress_api.spec.js │ │ ├── files.spec.js │ │ ├── local_storage.spec.js │ │ ├── location.spec.js │ │ ├── misc.spec.js │ │ ├── navigation.spec.js │ │ ├── network_requests.spec.js │ │ ├── querying.spec.js │ │ ├── spies_stubs_clocks.spec.js │ │ ├── traversal.spec.js │ │ ├── utilities.spec.js │ │ ├── viewport.spec.js │ │ ├── waiting.spec.js │ │ └── window.spec.js │ └── react-chrono │ │ ├── horizontal_spec.js │ │ ├── vertical_alternating_nomedia.js │ │ ├── vertical_alternating_spec.js │ │ └── vertical_spec.js ├── fixtures │ ├── example.json │ ├── profile.json │ └── users.json ├── plugins │ └── index.js ├── support │ ├── commands.js │ ├── commands.ts │ ├── component-index.html │ ├── component.ts │ └── e2e.js └── tsconfig.json ├── eslint.config.mjs ├── index.html ├── open_issues_last_1_year.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── camera.svg ├── color-circle.svg ├── d-day.mp4 ├── dunkirk.mp4 ├── favicon.ico ├── github copy.svg ├── github.svg ├── grid.svg ├── list.svg ├── manifest.json ├── notification-bell.svg ├── operation-barbarasso.mp4 ├── pearl-harbor.mp4 ├── robots.txt ├── rss.svg ├── satellite-dish.svg ├── zap.svg └── zapold.svg ├── readme-assets ├── horizontal_all.jpg ├── social-logo-small.png ├── text_overlay.jpg ├── vertical_alternating.jpg └── vertical_basic.jpg ├── rollup.config.mjs ├── sonar-project.properties ├── src ├── components │ ├── GlobalContext.tsx │ ├── __tests__ │ │ └── GlobalContext.test.tsx │ ├── common │ │ ├── styles │ │ │ └── index.ts │ │ ├── test │ │ │ └── index.tsx │ │ └── themes │ │ │ └── index.ts │ ├── contexts │ │ ├── DynamicContext.tsx │ │ ├── OptimizedContextProvider.tsx │ │ ├── StableContext.tsx │ │ ├── hooks.tsx │ │ ├── index.tsx │ │ └── legacy-types.tsx │ ├── effects │ │ ├── __tests__ │ │ │ ├── useClickOutside.test.tsx │ │ │ ├── useMatchMedia.test.tsx │ │ │ └── useSlideshow.test.ts │ │ ├── useCloseClickOutside.ts │ │ ├── useMatchMedia.ts │ │ ├── useNewScrollPosition.ts │ │ └── useSlideshow.ts │ ├── elements │ │ ├── list │ │ │ ├── __tests__ │ │ │ │ ├── list-item.test.tsx │ │ │ │ └── list.test.tsx │ │ │ ├── list-item.tsx │ │ │ ├── list.model.ts │ │ │ ├── list.styles.ts │ │ │ └── list.tsx │ │ └── popover │ │ │ ├── __tests__ │ │ │ └── popover.test.tsx │ │ │ ├── index.tsx │ │ │ ├── popover.model.ts │ │ │ └── popover.styles.ts │ ├── icons │ │ ├── arrow-down.tsx │ │ ├── check.tsx │ │ ├── chev-down.tsx │ │ ├── chev-left.tsx │ │ ├── chev-right.tsx │ │ ├── chev-up.tsx │ │ ├── chevs-left.tsx │ │ ├── chevs-right.tsx │ │ ├── close.tsx │ │ ├── index.tsx │ │ ├── layout.tsx │ │ ├── maximize.tsx │ │ ├── menu.tsx │ │ ├── minimize.tsx │ │ ├── minus.tsx │ │ ├── moon.tsx │ │ ├── para.tsx │ │ ├── plus.tsx │ │ ├── replay-icon.tsx │ │ ├── stop.tsx │ │ ├── sun.tsx │ │ ├── text.tsx │ │ └── triangle-right.tsx │ ├── index.tsx │ ├── timeline-elements │ │ ├── memoized │ │ │ ├── __tests__ │ │ │ │ └── index.test.tsx │ │ │ ├── details-text-memo.tsx │ │ │ ├── expand-button-memo.tsx │ │ │ ├── memoized-model.ts │ │ │ ├── show-hide-button.tsx │ │ │ ├── subtitle-memo.tsx │ │ │ └── title-memo.tsx │ │ ├── nested-timeline-renderer │ │ │ └── nested-timeline-renderer.tsx │ │ ├── timeline-card-content │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── content-footer.test.tsx.snap │ │ │ │ │ └── content-header.test.tsx.snap │ │ │ │ ├── content-footer.test.tsx │ │ │ │ ├── content-header.test.tsx │ │ │ │ ├── details-text.test.tsx │ │ │ │ ├── text_or_content.test.tsx │ │ │ │ └── timeline-card-content.test.tsx │ │ │ ├── card-animations.styles.ts │ │ │ ├── content-footer.tsx │ │ │ ├── content-header.tsx │ │ │ ├── details-text.model.ts │ │ │ ├── details-text.tsx │ │ │ ├── header-footer.model.ts │ │ │ ├── text-or-content.tsx │ │ │ ├── timeline-card-content.styles.ts │ │ │ └── timeline-card-content.tsx │ │ ├── timeline-card-media │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── timeline-card-media.test.tsx.snap │ │ │ │ └── timeline-card-media.test.tsx │ │ │ ├── components │ │ │ │ ├── ContentDisplay.tsx │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ ├── ImageDisplay.tsx │ │ │ │ ├── MediaContent.tsx │ │ │ │ ├── VideoPlayer.tsx │ │ │ │ └── YoutubePlayer.tsx │ │ │ ├── hooks │ │ │ │ ├── useMediaLoad.ts │ │ │ │ ├── useToggleControls.ts │ │ │ │ ├── useViewOptions.ts │ │ │ │ └── useYouTubeDetection.ts │ │ │ ├── timeline-card-media-buttons.tsx │ │ │ ├── timeline-card-media.styles.ts │ │ │ ├── timeline-card-media.tsx │ │ │ └── video.tsx │ │ ├── timeline-card │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── timeline-horizontal-card.test.tsx.snap │ │ │ │ └── timeline-horizontal-card.test.tsx │ │ │ ├── hooks │ │ │ │ └── useTimelineCard.ts │ │ │ ├── timeline-card-portal │ │ │ │ └── timeline-card-portal.tsx │ │ │ ├── timeline-horizontal-card.styles.ts │ │ │ ├── timeline-horizontal-card.tsx │ │ │ └── timeline-point │ │ │ │ └── timeline-point.tsx │ │ ├── timeline-control │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── timeline-control.test.tsx.snap │ │ │ │ └── timeline-control.test.tsx │ │ │ ├── timeline-control.styles.ts │ │ │ ├── timeline-control.styles.tsx │ │ │ └── timeline-control.tsx │ │ ├── timeline-item-title │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── timeline-card-title.test.tsx.snap │ │ │ │ └── timeline-card-title.test.tsx │ │ │ ├── timeline-card-title.styles.ts │ │ │ └── timeline-card-title.tsx │ │ └── timeline-outline │ │ │ ├── __tests__ │ │ │ └── timeline-outline.test.tsx │ │ │ ├── animations.ts │ │ │ ├── hooks │ │ │ └── useOutlinePosition.ts │ │ │ ├── timeline-outline-item-list.tsx │ │ │ ├── timeline-outline.model.ts │ │ │ ├── timeline-outline.styles.ts │ │ │ └── timeline-outline.tsx │ ├── timeline-horizontal │ │ ├── timeline-horizontal.styles.ts │ │ └── timeline-horizontal.tsx │ ├── timeline-vertical │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── timeline-point.test.tsx.snap │ │ │ │ └── timeline-vertical-item.test.tsx.snap │ │ │ ├── timeline-point.test.tsx │ │ │ └── timeline-vertical-item.test.tsx │ │ ├── timeline-point.tsx │ │ ├── timeline-vertical-item.tsx │ │ ├── timeline-vertical-shape.styles.ts │ │ ├── timeline-vertical.styles.ts │ │ └── timeline-vertical.tsx │ ├── timeline │ │ ├── TimelineView.tsx │ │ ├── __tests__ │ │ │ ├── layout-switcher.test.tsx │ │ │ └── timeline.test.tsx │ │ ├── timeline-popover-elements.tsx │ │ ├── timeline-popover.model.ts │ │ ├── timeline-toolbar.model.ts │ │ ├── timeline-toolbar.tsx │ │ ├── timeline.style.ts │ │ ├── timeline.style.tsx │ │ └── timeline.tsx │ ├── toggle-button │ │ ├── index.tsx │ │ └── toggle-button.styles.ts │ └── toolbar │ │ ├── __tests__ │ │ └── toolbar.test.tsx │ │ ├── index.tsx │ │ └── toolbar.styles.ts ├── demo │ ├── App.css │ ├── App.styles.ts │ ├── App.tsx │ ├── README.md │ ├── components │ │ ├── ThemeShowcase.tsx │ │ ├── horizontal │ │ │ ├── AllHorizontal.tsx │ │ │ ├── BasicHorizontal.tsx │ │ │ ├── CardlessHorizontal.tsx │ │ │ ├── InitialSelectedHorizontal.tsx │ │ │ └── index.ts │ │ ├── index.ts │ │ └── vertical │ │ │ ├── AlternatingNestedVertical.tsx │ │ │ ├── AlternatingVertical.tsx │ │ │ ├── BasicVertical.tsx │ │ │ ├── CardlessVertical.tsx │ │ │ ├── CustomContentVertical.tsx │ │ │ ├── CustomContentWithIconsVertical.tsx │ │ │ ├── MixedVertical.tsx │ │ │ ├── NestedVertical.tsx │ │ │ ├── NewMediaVertical.tsx │ │ │ └── index.ts │ ├── custom-theme-example.tsx │ ├── data │ │ ├── basic-timeline.ts │ │ ├── index.ts │ │ ├── mixed-timeline.ts │ │ ├── nested-timeline.ts │ │ └── world-history.ts │ ├── dynamic-load.tsx │ ├── layout.module.scss │ └── layout.tsx ├── examples │ ├── basic-horizontal.tsx │ ├── basic-slideshow.tsx │ ├── basic-vertical.tsx │ ├── data.ts │ └── slideshow-with-overall-progress.tsx ├── hooks │ ├── __tests__ │ │ ├── useBackground.test.ts │ │ ├── useCardSize.test.ts │ │ ├── useEscapeKey.test.ts │ │ ├── useMediaState.test.ts │ │ ├── useOutsideClick.test.ts │ │ ├── useTimelineMedia.test.ts │ │ ├── useTimelineMode.test.ts │ │ ├── useTimelineNavigation.test.ts │ │ ├── useTimelineScroll.test.ts │ │ ├── useTimelineSearch.test.ts │ │ ├── useUIState.test.ts │ │ └── useWindowSize.test.ts │ ├── useBackground.ts │ ├── useCardSize.ts │ ├── useEscapeKey.ts │ ├── useMediaState.ts │ ├── useOutsideClick.ts │ ├── useSlideshowProgress.ts │ ├── useTimelineMedia.ts │ ├── useTimelineMode.ts │ ├── useTimelineNavigation.ts │ ├── useTimelineScroll.ts │ ├── useTimelineSearch.ts │ ├── useUIState.ts │ └── useWindowSize.ts ├── index.css ├── index.tsx ├── models │ ├── Theme.ts │ ├── TimelineCardTitleModel.ts │ ├── TimelineContentModel.ts │ ├── TimelineControlModel.ts │ ├── TimelineHorizontalModel.ts │ ├── TimelineItemModel.ts │ ├── TimelineMediaModel.ts │ ├── TimelineModel.ts │ ├── TimelineVerticalModel.ts │ ├── ToolbarItem.ts │ └── ToolbarProps.ts ├── react-app-env.d.ts ├── react-chrono.ts ├── test-setup.js ├── types │ └── global.d.ts └── utils │ ├── index.ts │ ├── mediaQueryUtils.test.ts │ ├── mediaQueryUtils.ts │ ├── performance.test.ts │ ├── performance.ts │ ├── timelineUtils.test.ts │ ├── timelineUtils.ts │ └── utils.test.ts ├── stats.html ├── tailwind.config.js ├── tsconfig.json ├── types └── static.d.ts ├── vite.config.mts ├── vitest.config.mts └── webpack.config.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 60, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "aloisdg", 10 | "name": "Alois", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/3449303?v=4", 12 | "profile": "http://aloisdg.github.io/", 13 | "contributions": [ 14 | "doc" 15 | ] 16 | }, 17 | { 18 | "login": "koji", 19 | "name": "Koji", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/474225?v=4", 21 | "profile": "https://kojikoji.ga", 22 | "contributions": [ 23 | "doc" 24 | ] 25 | }, 26 | { 27 | "login": "alx", 28 | "name": "Alexandre Girard", 29 | "avatar_url": "https://avatars.githubusercontent.com/u/373?v=4", 30 | "profile": "http://alexgirard.com", 31 | "contributions": [ 32 | "code" 33 | ] 34 | }, 35 | { 36 | "login": "Ryuyxx", 37 | "name": "Ryuya", 38 | "avatar_url": "https://avatars.githubusercontent.com/u/69892552?v=4", 39 | "profile": "https://github.com/Ryuyxx", 40 | "contributions": [ 41 | "doc" 42 | ] 43 | }, 44 | { 45 | "login": "taqi457", 46 | "name": "Taqi Abbas", 47 | "avatar_url": "https://avatars.githubusercontent.com/u/2155219?v=4", 48 | "profile": "https://github.com/taqi457", 49 | "contributions": [ 50 | "code" 51 | ] 52 | }, 53 | { 54 | "login": "megasoft78", 55 | "name": "megasoft78", 56 | "avatar_url": "https://avatars.githubusercontent.com/u/514958?v=4", 57 | "profile": "https://github.com/megasoft78", 58 | "contributions": [ 59 | "code" 60 | ] 61 | }, 62 | { 63 | "login": "bigbigDreamer", 64 | "name": "Eric(书生)", 65 | "avatar_url": "https://avatars.githubusercontent.com/u/39019913?v=4", 66 | "profile": "https://dev.bigdreamer.cc", 67 | "contributions": [ 68 | "code" 69 | ] 70 | }, 71 | { 72 | "login": "DrakeXorn", 73 | "name": "Christophe Bernard", 74 | "avatar_url": "https://avatars.githubusercontent.com/u/3925261?v=4", 75 | "profile": "https://github.com/DrakeXorn", 76 | "contributions": [ 77 | "code" 78 | ] 79 | } 80 | ], 81 | "contributorsPerLine": 7, 82 | "projectName": "react-chrono", 83 | "projectOwner": "prabhuignoto", 84 | "repoType": "github", 85 | "repoHost": "https://github.com", 86 | "skipCi": true, 87 | "commitConvention": "angular" 88 | } 89 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | exclude_patterns = [ 4 | "**/node_modules/", 5 | "dist/**", 6 | "src/examples/**", 7 | "src/demo/**" 8 | ] 9 | 10 | [[analyzers]] 11 | name = "javascript" 12 | enabled = true 13 | 14 | [analyzers.meta] 15 | environment = ["browser"] 16 | plugins = ["react"] 17 | style_guide = "standard" 18 | dialect = "typescript" 19 | -------------------------------------------------------------------------------- /.depcheckrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignores": [ 3 | "eslint", 4 | "babel-*", 5 | "@rollup/*", 6 | "react", 7 | "react-dom", 8 | "rollup-plugin-*" 9 | ], 10 | "skip-missing": true, 11 | "ignore-bin-package": false, 12 | "ignore-dirs": [ 13 | "dist", 14 | "coverage" 15 | ], 16 | "ignore-path": ".eslintignore", 17 | "parsers": { 18 | "*.js": "es6", 19 | "*.jsx": "jsx", 20 | "*.ts": "typescript", 21 | "*.tsx": "tsx" 22 | }, 23 | "specials": [ 24 | "bin", 25 | "eslint", 26 | "mocha" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: prabhuignoto 3 | ko_fi: prabhuignoto 4 | buy_me_a_coffee: prabhuignoto 5 | custom: ['paypal.me/prabhuignoto'] 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[FEAT] Brief description of the feature' 5 | labels: 'enhancement, needs-discussion' 6 | assignees: '' 7 | --- 8 | 9 | **Before You Submit** 10 | 11 | - [ ] Please search for existing feature requests (open and closed) to avoid creating a duplicate. 12 | - [ ] Ensure this feature aligns with the project's overall goals and scope (if known). 13 | 14 | **1. The Problem or User Story** 15 | _Is your feature request related to a problem? Or is it a new idea?_ 16 | 17 | - Please describe the problem this feature would solve OR provide a user story. 18 | - **User Story Example:** "As a [type of user], I want to [perform an action] so that I can [achieve this benefit/goal]." 19 | - **2. Proposed Solution** 20 | _Describe the solution you'd like in detail._ 21 | 22 | - How would this feature work? 23 | - What would it look like? (Feel free to include mockups, diagrams, or detailed descriptions in the "Additional Context" section). 24 | - **3. Value & Benefits** 25 | _Why is this feature important? What value would it bring?_ 26 | 27 | - Who would benefit from this feature? 28 | - How would it improve the project or user experience? 29 | - **4. Alternatives Considered (Optional)** 30 | _Have you thought of other ways to address the problem or achieve the goal?_ 31 | 32 | - A clear and concise description of any alternative solutions or features you've considered. 33 | - Why is your proposed solution the best fit? 34 | - **5. Success Metrics (Optional)** 35 | _How would we know if this feature is successful after implementation?_ 36 | 37 | - e.g., Increased user engagement in X area, Reduced time to complete Y task, Positive feedback on Z aspect. 38 | - **6. Accessibility Considerations (if applicable)** 39 | _Please describe any accessibility aspects related to this feature._ 40 | 41 | - Will this feature be accessible to users with disabilities (e.g., screen reader compatibility, keyboard navigation, color contrast)? 42 | - **7. Additional Context** 43 | _Add any other context, screenshots, mockups, or links about the feature request here._ 44 | 45 | - 46 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build-and-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [20.x] 16 | 17 | steps: 18 | - uses: pnpm/action-setup@v4 19 | with: 20 | version: 8.8.0 21 | 22 | - uses: actions/checkout@v4 23 | 24 | - name: Cache PNPM dependencies 25 | uses: actions/cache@v4 26 | with: 27 | path: ~/.pnpm-store 28 | key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} 29 | restore-keys: | 30 | ${{ runner.os }}-pnpm- 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | 37 | - name: Install Dependencies 38 | run: pnpm install 39 | 40 | - name: Cache build artifacts 41 | uses: actions/cache@v4 42 | with: 43 | path: dist 44 | key: ${{ runner.os }}-build-${{ matrix.node-version }} 45 | restore-keys: | 46 | ${{ runner.os }}-build- 47 | 48 | - name: Build 49 | run: pnpm rollup 50 | 51 | - name: Unit Tests 52 | run: pnpm test 53 | -------------------------------------------------------------------------------- /.github/workflows/coveralls.yml: -------------------------------------------------------------------------------- 1 | name: Coveralls 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | upload: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2.4.2 14 | 15 | - name: Cache PNPM dependencies 16 | uses: actions/cache@v4 17 | with: 18 | path: ~/.pnpm-store 19 | key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} 20 | restore-keys: | 21 | ${{ runner.os }}-pnpm- 22 | 23 | - name: Set up pnpm 24 | uses: pnpm/action-setup@v4 25 | with: 26 | version: latest 27 | 28 | - name: Install dependencies 29 | run: pnpm install 30 | 31 | - name: Run tests 32 | run: pnpm test 33 | 34 | - name: Upload coverage report 35 | env: 36 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_SECRET }} 37 | run: pnpm run coveralls 38 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yaml: -------------------------------------------------------------------------------- 1 | name: Snyk Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | snyk-security-scan: 13 | runs-on: ubuntu-latest 14 | name: Snyk Security Scan 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup PNPM 19 | uses: pnpm/action-setup@v4 20 | with: 21 | version: latest 22 | 23 | - name: Cache PNPM dependencies 24 | uses: actions/cache@v4 25 | with: 26 | path: ~/.pnpm-store 27 | key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} 28 | restore-keys: | 29 | ${{ runner.os }}-pnpm- 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Cache Snyk cache folder 35 | uses: actions/cache@v4 36 | with: 37 | path: ~/.cache/snyk 38 | key: ${{ runner.os }}-snyk-${{ hashFiles('pnpm-lock.yaml') }} 39 | restore-keys: | 40 | ${{ runner.os }}-snyk- 41 | 42 | - name: Run Snyk to check for vulnerabilities 43 | uses: snyk/actions/node@master 44 | env: 45 | SNYK_TOKEN: ${{ secrets.SNYK_SECRET }} 46 | with: 47 | args: --severity-threshold=high 48 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: Sonar scan (disabled) 2 | # Workflow disabled. Uncomment the 'on:' section below to re-enable. 3 | # on: 4 | # push: 5 | # branches: 6 | # - master 7 | # pull_request: 8 | # types: [opened, synchronize, reopened] 9 | jobs: 10 | sonarcloud: 11 | name: SonarCloud 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 17 | - name: SonarCloud Scan 18 | uses: SonarSource/sonarcloud-github-action@master 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 21 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | # /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /dist 26 | /dist_site 27 | 28 | storybook-static 29 | 30 | debug 31 | .vscode 32 | .husky 33 | cache 34 | .dccache 35 | .idea 36 | .tmp 37 | 38 | cypress/videos 39 | 40 | .scannerwork 41 | # sonar-project.properties 42 | react-app-tester 43 | 44 | react-test-bed 45 | # Ignore Gradle project-specific cache directory 46 | .gradle 47 | 48 | # Ignore Gradle build output directory 49 | build 50 | 51 | ## Panda 52 | styled-system 53 | styled-system-studio 54 | 55 | venv 56 | extract_sonar_issues.py 57 | sonarqube_issues.csv 58 | requirements.txt -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | env 2 | icons 3 | src/demo 4 | src/examples -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.restyled.yaml: -------------------------------------------------------------------------------- 1 | exclude: 2 | - "**/*.patch" 3 | - "**/node_modules/**/*" 4 | - "**/vendor/**/*" 5 | - ".github/workflows/**/*" 6 | - ".gitignore" 7 | - "pnpm-lock.yaml" 8 | - "**/tsconfig.json" 9 | - coverage/**/* 10 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.22.1 3 | ignore: {} 4 | patch: {} 5 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "customSyntax": "postcss-styled-syntax", 3 | "rules": {}, 4 | "ignoreFiles": ["node_modules/**/*", "dist/**/*"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deepscan.ignoreConfirmWarning": true, 3 | "typescript.tsdk": "node_modules\\typescript\\lib", 4 | "cSpell.words": [ 5 | "actv", 6 | "chrono", 7 | "debounced", 8 | "debounced actv timeline item", 9 | "item", 10 | "timeline" 11 | ], 12 | "sonarlint.connectedMode.project": { 13 | "connectionId": "prabhuignoto", 14 | "projectKey": "prabhuignoto_react-chrono" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at https://www.contributor-covenant.org/version/1/0/0/code-of-conduct.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Fred K. Schott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | '@babel/plugin-transform-optional-chaining', 4 | '@babel/plugin-transform-typescript', 5 | 'babel-plugin-jsx-remove-data-test-id', 6 | ], 7 | // Adding presets for better compatibility and support 8 | presets: [ 9 | '@babel/preset-env', 10 | '@babel/preset-react', 11 | '@babel/preset-typescript', 12 | ], 13 | }; -------------------------------------------------------------------------------- /browserstack.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth": { 3 | "username": "prabhu40", 4 | "access_key": "kdhsu2aDyBguV7cTCqZ4" 5 | }, 6 | "browsers": [ 7 | { 8 | "browser": "chrome", 9 | "os": "Windows 10", 10 | "versions": [ 11 | "latest", 12 | "latest-1" 13 | ] 14 | } 15 | ], 16 | "run_settings": { 17 | "cypress_config_file": "./cypress.json", 18 | "project_name": "project-name", 19 | "build_name": "build-name", 20 | "exclude": [], 21 | "parallels": "5", 22 | "npm_dependencies": { 23 | "@babel/plugin-proposal-optional-chaining": "^7.12.1", 24 | "@babel/core": "^7.12.3", 25 | "@babel/preset-react": "^7.10.4", 26 | "@babel/preset-typescript": "^7.10.4" 27 | }, 28 | "package_config_options": {} 29 | }, 30 | "connection_settings": { 31 | "local": true, 32 | "local_identifier": null 33 | }, 34 | "disable_usage_reporting": false 35 | } -------------------------------------------------------------------------------- /coverage/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/coverage/favicon.png -------------------------------------------------------------------------------- /coverage/lcov-report/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/coverage/lcov-report/favicon.png -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /coverage/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /coverage/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/coverage/sort-arrow-sprite.png -------------------------------------------------------------------------------- /cypress/e2e/examples/aliasing.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Aliasing', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/aliasing'); 6 | }); 7 | 8 | it('.as() - alias a DOM element for later use', () => { 9 | // https://on.cypress.io/as 10 | 11 | // Alias a DOM element for use later 12 | // We don't have to traverse to the element 13 | // later in our code, we reference it with @ 14 | 15 | cy.get('.as-table') 16 | .find('tbody>tr') 17 | .first() 18 | .find('td') 19 | .first() 20 | .find('button') 21 | .as('firstBtn'); 22 | 23 | // when we reference the alias, we place an 24 | // @ in front of its name 25 | cy.get('@firstBtn').click(); 26 | 27 | cy.get('@firstBtn') 28 | .should('have.class', 'btn-success') 29 | .and('contain', 'Changed'); 30 | }); 31 | 32 | it('.as() - alias a route for later use', () => { 33 | // Alias the route to wait for its response 34 | cy.server(); 35 | cy.route('GET', 'comments/*').as('getComment'); 36 | 37 | // we have code that gets a comment when 38 | // the button is clicked in scripts.js 39 | cy.get('.network-btn').click(); 40 | 41 | // https://on.cypress.io/wait 42 | cy.wait('@getComment').its('status').should('eq', 200); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /cypress/e2e/examples/local_storage.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Local Storage', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/local-storage'); 6 | }); 7 | // Although local storage is automatically cleared 8 | // in between tests to maintain a clean state 9 | // sometimes we need to clear the local storage manually 10 | 11 | it('cy.clearLocalStorage() - clear all data in local storage', () => { 12 | // https://on.cypress.io/clearlocalstorage 13 | cy.get('.ls-btn') 14 | .click() 15 | .should(() => { 16 | expect(localStorage.getItem('prop1')).to.eq('red'); 17 | expect(localStorage.getItem('prop2')).to.eq('blue'); 18 | expect(localStorage.getItem('prop3')).to.eq('magenta'); 19 | }); 20 | 21 | // clearLocalStorage() yields the localStorage object 22 | cy.clearLocalStorage().should((ls) => { 23 | expect(ls.getItem('prop1')).to.be.null; 24 | expect(ls.getItem('prop2')).to.be.null; 25 | expect(ls.getItem('prop3')).to.be.null; 26 | }); 27 | 28 | // Clear key matching string in Local Storage 29 | cy.get('.ls-btn') 30 | .click() 31 | .should(() => { 32 | expect(localStorage.getItem('prop1')).to.eq('red'); 33 | expect(localStorage.getItem('prop2')).to.eq('blue'); 34 | expect(localStorage.getItem('prop3')).to.eq('magenta'); 35 | }); 36 | 37 | cy.clearLocalStorage('prop1').should((ls) => { 38 | expect(ls.getItem('prop1')).to.be.null; 39 | expect(ls.getItem('prop2')).to.eq('blue'); 40 | expect(ls.getItem('prop3')).to.eq('magenta'); 41 | }); 42 | 43 | // Clear keys matching regex in Local Storage 44 | cy.get('.ls-btn') 45 | .click() 46 | .should(() => { 47 | expect(localStorage.getItem('prop1')).to.eq('red'); 48 | expect(localStorage.getItem('prop2')).to.eq('blue'); 49 | expect(localStorage.getItem('prop3')).to.eq('magenta'); 50 | }); 51 | 52 | cy.clearLocalStorage(/prop1|2/).should((ls) => { 53 | expect(ls.getItem('prop1')).to.be.null; 54 | expect(ls.getItem('prop2')).to.be.null; 55 | expect(ls.getItem('prop3')).to.eq('magenta'); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /cypress/e2e/examples/location.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Location', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/location'); 6 | }); 7 | 8 | it('cy.hash() - get the current URL hash', () => { 9 | // https://on.cypress.io/hash 10 | cy.hash().should('be.empty'); 11 | }); 12 | 13 | it('cy.location() - get window.location', () => { 14 | // https://on.cypress.io/location 15 | cy.location().should((location) => { 16 | expect(location.hash).to.be.empty; 17 | expect(location.href).to.eq( 18 | 'https://example.cypress.io/commands/location', 19 | ); 20 | expect(location.host).to.eq('example.cypress.io'); 21 | expect(location.hostname).to.eq('example.cypress.io'); 22 | expect(location.origin).to.eq('https://example.cypress.io'); 23 | expect(location.pathname).to.eq('/commands/location'); 24 | expect(location.port).to.eq(''); 25 | expect(location.protocol).to.eq('https:'); 26 | expect(location.search).to.be.empty; 27 | }); 28 | }); 29 | 30 | it('cy.url() - get the current URL', () => { 31 | // https://on.cypress.io/url 32 | cy.url().should('eq', 'https://example.cypress.io/commands/location'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /cypress/e2e/examples/navigation.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Navigation', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io'); 6 | cy.get('.navbar-nav').contains('Commands').click(); 7 | cy.get('.dropdown-menu').contains('Navigation').click(); 8 | }); 9 | 10 | it("cy.go() - go back or forward in the browser's history", () => { 11 | // https://on.cypress.io/go 12 | 13 | cy.location('pathname').should('include', 'navigation'); 14 | 15 | cy.go('back'); 16 | cy.location('pathname').should('not.include', 'navigation'); 17 | 18 | cy.go('forward'); 19 | cy.location('pathname').should('include', 'navigation'); 20 | 21 | // clicking back 22 | cy.go(-1); 23 | cy.location('pathname').should('not.include', 'navigation'); 24 | 25 | // clicking forward 26 | cy.go(1); 27 | cy.location('pathname').should('include', 'navigation'); 28 | }); 29 | 30 | it('cy.reload() - reload the page', () => { 31 | // https://on.cypress.io/reload 32 | cy.reload(); 33 | 34 | // reload the page without using the cache 35 | cy.reload(true); 36 | }); 37 | 38 | it('cy.visit() - visit a remote url', () => { 39 | // https://on.cypress.io/visit 40 | 41 | // Visit any sub-domain of your current domain 42 | 43 | // Pass options to the visit 44 | cy.visit('https://example.cypress.io/commands/navigation', { 45 | timeout: 50000, // increase total time for the visit to resolve 46 | onBeforeLoad(contentWindow) { 47 | // contentWindow is the remote page's window object 48 | expect(typeof contentWindow === 'object').to.be.true; 49 | }, 50 | onLoad(contentWindow) { 51 | // contentWindow is the remote page's window object 52 | expect(typeof contentWindow === 'object').to.be.true; 53 | }, 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /cypress/e2e/examples/viewport.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Viewport', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/viewport'); 6 | }); 7 | 8 | it('cy.viewport() - set the viewport size and dimension', () => { 9 | // https://on.cypress.io/viewport 10 | 11 | cy.get('#navbar').should('be.visible'); 12 | cy.viewport(320, 480); 13 | 14 | // the navbar should have collapse since our screen is smaller 15 | cy.get('#navbar').should('not.be.visible'); 16 | cy.get('.navbar-toggle').should('be.visible').click(); 17 | cy.get('.nav').find('a').should('be.visible'); 18 | 19 | // lets see what our app looks like on a super large screen 20 | cy.viewport(2999, 2999); 21 | 22 | // cy.viewport() accepts a set of preset sizes 23 | // to easily set the screen to a device's width and height 24 | 25 | // We added a cy.wait() between each viewport change so you can see 26 | // the change otherwise it is a little too fast to see :) 27 | 28 | cy.viewport('macbook-15'); 29 | cy.wait(200); 30 | cy.viewport('macbook-13'); 31 | cy.wait(200); 32 | cy.viewport('macbook-11'); 33 | cy.wait(200); 34 | cy.viewport('ipad-2'); 35 | cy.wait(200); 36 | cy.viewport('ipad-mini'); 37 | cy.wait(200); 38 | cy.viewport('iphone-6+'); 39 | cy.wait(200); 40 | cy.viewport('iphone-6'); 41 | cy.wait(200); 42 | cy.viewport('iphone-5'); 43 | cy.wait(200); 44 | cy.viewport('iphone-4'); 45 | cy.wait(200); 46 | cy.viewport('iphone-3'); 47 | cy.wait(200); 48 | 49 | // cy.viewport() accepts an orientation for all presets 50 | // the default orientation is 'portrait' 51 | cy.viewport('ipad-2', 'portrait'); 52 | cy.wait(200); 53 | cy.viewport('iphone-4', 'landscape'); 54 | cy.wait(200); 55 | 56 | // The viewport will be reset back to the default dimensions 57 | // in between tests (the default can be set in cypress.json) 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /cypress/e2e/examples/waiting.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Waiting', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/waiting'); 6 | }); 7 | // BE CAREFUL of adding unnecessary wait times. 8 | // https://on.cypress.io/best-practices#Unnecessary-Waiting 9 | 10 | // https://on.cypress.io/wait 11 | it('cy.wait() - wait for a specific amount of time', () => { 12 | cy.get('.wait-input1').type('Wait 1000ms after typing'); 13 | cy.wait(1000); 14 | cy.get('.wait-input2').type('Wait 1000ms after typing'); 15 | cy.wait(1000); 16 | cy.get('.wait-input3').type('Wait 1000ms after typing'); 17 | cy.wait(1000); 18 | }); 19 | 20 | it('cy.wait() - wait for a specific route', () => { 21 | cy.server(); 22 | 23 | // Listen to GET to comments/1 24 | cy.route('GET', 'comments/*').as('getComment'); 25 | 26 | // we have code that gets a comment when 27 | // the button is clicked in scripts.js 28 | cy.get('.network-btn').click(); 29 | 30 | // wait for GET comments/1 31 | cy.wait('@getComment').its('status').should('eq', 200); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /cypress/e2e/examples/window.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Window', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/window'); 6 | }); 7 | 8 | it('cy.window() - get the global window object', () => { 9 | // https://on.cypress.io/window 10 | cy.window().should('have.property', 'top'); 11 | }); 12 | 13 | it('cy.document() - get the document object', () => { 14 | // https://on.cypress.io/document 15 | cy.document().should('have.property', 'charset').and('eq', 'UTF-8'); 16 | }); 17 | 18 | it('cy.title() - get the title', () => { 19 | // https://on.cypress.io/title 20 | cy.title().should('include', 'Kitchen Sink'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/e2e/react-chrono/horizontal_spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Chrono.Horizontal.Basic', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:4444/horizontal'); 6 | }); 7 | 8 | it('check length', () => { 9 | cy.get('.timeline-horz-item-container').should('have.length', 10); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /cypress/e2e/react-chrono/vertical_alternating_nomedia.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Chrono.Vertical.Alternating', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:4444/vertical-alternating'); 6 | }); 7 | 8 | it('check length', () => { 9 | cy.get('.vertical-item-row').should('have.length', 13); 10 | }); 11 | 12 | it('check card elements', () => { 13 | cy.get('.vertical-item-row').first().children().should('have.length', 3); 14 | }); 15 | 16 | it('check timeline title for first item', () => { 17 | cy.get('.vertical-item-row>div').eq(1).last().should('contain', 'May 1940'); 18 | }); 19 | 20 | it('check timeline card contents', () => { 21 | cy.get('.vertical-item-row') 22 | .eq(3) 23 | .find('.timeline-card-content') 24 | .within(() => { 25 | cy.get('.rc-card-title').should('contain', 'Pearl Harbor'); 26 | cy.get('.rc-card-subtitle').should( 27 | 'contain', 28 | 'The destroyer USS Shaw explodes in dry dock after being hit by Japanese aircraft', 29 | ); 30 | }); 31 | }); 32 | 33 | it('check card title', () => { 34 | cy.get('.vertical-item-row') 35 | .eq(0) 36 | .find('.rc-card-title') 37 | .should('contain', 'Dunkirk'); 38 | }); 39 | 40 | it('check card description', () => { 41 | cy.get('.vertical-item-row') 42 | .eq(0) 43 | .find('.card-description>p') 44 | .should( 45 | 'contain', 46 | 'On 10 May 1940, Hitler began his long-awaited offensive in the west', 47 | ); 48 | }); 49 | 50 | it('check card sub title', () => { 51 | cy.get('.vertical-item-row') 52 | .eq(1) 53 | .find('.rc-card-subtitle') 54 | .should('contain', 'RAF Spitfire pilots scramble for their planes'); 55 | }); 56 | 57 | it('check card active', () => { 58 | cy.get('.vertical-item-row') 59 | .eq(1) 60 | .find('.timeline-card-content') 61 | .click() 62 | .should('have.class', 'active'); 63 | }); 64 | 65 | it('check scroll', () => { 66 | cy.get('.timeline-main-wrapper').scrollTo('bottom'); 67 | cy.wait(1000); 68 | cy.get('.vertical-item-row') 69 | .last() 70 | .find('.card-content-wrapper') 71 | .should('have.class', 'visible'); 72 | 73 | cy.get('.timeline-main-wrapper').scrollTo('top'); 74 | cy.wait(1000); 75 | cy.get('.vertical-item-row') 76 | .first() 77 | .find('.card-content-wrapper') 78 | .should('have.class', 'visible'); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/fixtures/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 8739, 3 | "name": "Jane", 4 | "email": "jane@example.com" 5 | } -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // Modern custom command example using cy.intercept() 12 | // Cypress.Commands.add('interceptApiCall', (method, url, alias) => { 13 | // cy.intercept(method, url).as(alias); 14 | // }); 15 | 16 | // Example of a custom command for testing React components 17 | // Cypress.Commands.add('getByDataTestId', (testId) => { 18 | // return cy.get(`[data-testid="${testId}"]`); 19 | // }); 20 | 21 | // Example of a custom command for accessibility testing 22 | // Cypress.Commands.add('checkA11y', (context, options) => { 23 | // cy.injectAxe(); 24 | // cy.checkA11y(context, options); 25 | // }); 26 | 27 | // TypeScript support for custom commands 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // interceptApiCall(method: string, url: string, alias: string): Chainable; 32 | // getByDataTestId(testId: string): Chainable; 33 | // checkA11y(context?: any, options?: any): Chainable; 34 | // } 35 | // } 36 | // } 37 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/react18' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | 38 | // Example use: 39 | // cy.mount() 40 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "cypress", 6 | "node" 7 | ], 8 | "target": "ES2022", 9 | "lib": ["ES2022", "DOM"], 10 | "module": "ESNext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "strict": true, 17 | "skipLibCheck": true, 18 | "rootDir": ".." 19 | }, 20 | "include": [ 21 | "../node_modules/cypress", 22 | "./**/*.ts", 23 | "./**/*.tsx", 24 | "../cypress.config.ts", 25 | "../src/**/*.ts", 26 | "../src/**/*.tsx" 27 | ], 28 | "exclude": [ 29 | "../node_modules", 30 | "../dist" 31 | ] 32 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; 2 | import _import from 'eslint-plugin-import'; 3 | import react from 'eslint-plugin-react'; 4 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 5 | import jsxA11Y from 'eslint-plugin-jsx-a11y'; 6 | import typescriptSortKeys from 'eslint-plugin-typescript-sort-keys'; 7 | import sortKeysFix from 'eslint-plugin-sort-keys-fix'; 8 | import globals from 'globals'; 9 | import tsParser from '@typescript-eslint/parser'; 10 | import path from 'node:path'; 11 | import { fileURLToPath } from 'node:url'; 12 | import js from '@eslint/js'; 13 | import { FlatCompat } from '@eslint/eslintrc'; 14 | 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = path.dirname(__filename); 17 | const compat = new FlatCompat({ 18 | allConfig: js.configs.all, 19 | baseDirectory: __dirname, 20 | recommendedConfig: js.configs.recommended, 21 | }); 22 | 23 | export default [ 24 | { 25 | ignores: ['src/demo/*', 'src/assets/*', 'src/examples/*'], 26 | }, 27 | ...fixupConfigRules( 28 | compat.extends( 29 | 'plugin:import/typescript', 30 | 'plugin:react/recommended', 31 | 'prettier', 32 | 'plugin:react/jsx-runtime', 33 | ), 34 | ), 35 | { 36 | languageOptions: { 37 | ecmaVersion: 12, 38 | 39 | globals: { 40 | ...globals.browser, 41 | }, 42 | parser: tsParser, 43 | parserOptions: { 44 | ecmaFeatures: { 45 | jsx: true, 46 | }, 47 | }, 48 | 49 | sourceType: 'module', 50 | }, 51 | 52 | plugins: { 53 | '@typescript-eslint': typescriptEslint, 54 | import: fixupPluginRules(_import), 55 | 'jsx-a11y': jsxA11Y, 56 | react: fixupPluginRules(react), 57 | 'sort-keys-fix': sortKeysFix, 58 | 'typescript-sort-keys': typescriptSortKeys, 59 | }, 60 | 61 | rules: { 62 | 'sort-keys-fix/sort-keys-fix': 'error', 63 | 'typescript-sort-keys/interface': 'error', 64 | 'typescript-sort-keys/string-enum': 'error', 65 | }, 66 | 67 | settings: { 68 | react: { 69 | version: '18.1.0', 70 | }, 71 | }, 72 | }, 73 | ]; 74 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Chrono 8 | 9 | 10 |
11 | 12 | 13 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/camera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/color-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/d-day.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/public/d-day.mp4 -------------------------------------------------------------------------------- /public/dunkirk.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/public/dunkirk.mp4 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/public/favicon.ico -------------------------------------------------------------------------------- /public/github copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/grid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/notification-bell.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/operation-barbarasso.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/public/operation-barbarasso.mp4 -------------------------------------------------------------------------------- /public/pearl-harbor.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/public/pearl-harbor.mp4 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/rss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/satellite-dish.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/zap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/zapold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /readme-assets/horizontal_all.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/readme-assets/horizontal_all.jpg -------------------------------------------------------------------------------- /readme-assets/social-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/readme-assets/social-logo-small.png -------------------------------------------------------------------------------- /readme-assets/text_overlay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/readme-assets/text_overlay.jpg -------------------------------------------------------------------------------- /readme-assets/vertical_alternating.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/readme-assets/vertical_alternating.jpg -------------------------------------------------------------------------------- /readme-assets/vertical_basic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prabhuignoto/react-chrono/235f9a131fd1112ff28b7d9e289e92763652c94a/readme-assets/vertical_basic.jpg -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=prabhuignoto_react-chrono 2 | sonar.organization=prabhuignoto 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=react-chrono 6 | #sonar.projectVersion=1.0 7 | 8 | sonar.sources=src 9 | sonar.exclusions=**/node_modules/**,**/*.test.js,**/*.spec.js,**/*.test.jsx,**/*.spec.jsx,**/*.test.ts,**/*.spec.ts,**/*.test.tsx,**/*.spec.tsx,coverage/**,build/**,dist/**,public/**,src/setupTests.js,src/setupTests.ts,src/setupTests.tsx,src/setupTests.jsx,src/setupTests.test.js,src/setupTests.test.ts,src/setupTests.test.tsx,src/setupTests.test.jsx,src/setupTests.spec.js,src/setupTests.spec.ts,src/setupTests.spec.tsx,src/setupTests.spec.jsx,src/serviceWorker.js,src/serviceWorker.ts,src/serviceWorker.tsx,src/serviceWorker.jsx,src/serviceWorker.test.js,src/serviceWorker.test.ts,src/serviceWorker.test.tsx,src/serviceWorker.test.jsx,src/serviceWorker.spec.js,src/serviceWorker.spec.ts,src/serviceWorker.spec.tsx,src/serviceWorker.spec.jsx,src/demo/**,src/components/**/*.style.ts,src/components/common/styles/index.ts,src/components/common/test/index.tsx 10 | 11 | # Coverage configuration 12 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 13 | sonar.coverage.exclusions=**/*.test.js,**/*.spec.js,**/*.test.jsx,**/*.spec.jsx,**/*.test.ts,**/*.spec.ts,**/*.test.tsx,**/*.spec.tsx,coverage/**,build/**,dist/**,public/**,src/setupTests.js,src/setupTests.ts,src/setupTests.tsx,src/setupTests.jsx,src/setupTests.test.js,src/setupTests.test.ts,src/setupTests.test.tsx,src/setupTests.test.jsx,src/setupTests.spec.js,src/setupTests.spec.ts,src/setupTests.spec.tsx,src/setupTests.spec.jsx,src/serviceWorker.js,src/serviceWorker.ts,src/serviceWorker.tsx,src/serviceWorker.jsx,src/serviceWorker.test.js,src/serviceWorker.test.ts,src/serviceWorker.test.tsx,src/serviceWorker.test.jsx,src/serviceWorker.spec.js,src/serviceWorker.spec.ts,src/serviceWorker.spec.tsx,src/serviceWorker.spec.jsx,src/demo/**,src/components/**/*.style.ts,src/components/common/styles/index.ts,src/components/common/test/index.tsx 14 | 15 | # TypeScript specific settings 16 | sonar.typescript.file.suffixes=ts,tsx 17 | sonar.typescript.tsconfigPath=tsconfig.json 18 | 19 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 20 | #sonar.sources=. 21 | 22 | # Encoding of the source code. Default is default system encoding 23 | #sonar.sourceEncoding=UTF-8 24 | -------------------------------------------------------------------------------- /src/components/common/styles/index.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@models/Theme'; 2 | import { css } from 'styled-components'; 3 | 4 | export const ScrollBar = css<{ theme: Theme }>` 5 | scrollbar-color: ${(p) => p.theme?.primary} default; 6 | scrollbar-width: thin; 7 | 8 | &::-webkit-scrollbar { 9 | width: 0.3em; 10 | } 11 | 12 | &::-webkit-scrollbar-track { 13 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.2); 14 | } 15 | 16 | &::-webkit-scrollbar-thumb { 17 | background-color: ${(p) => p.theme?.primary}; 18 | outline: 1px solid ${(p) => p.theme?.primary}; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/components/contexts/DynamicContext.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Dynamic Context - Contains frequently changing values like state and interactions 3 | * This context is optimized for dynamic values that change often 4 | */ 5 | import { createContext } from 'react'; 6 | import { TextDensity } from '@models/TimelineModel'; 7 | import { Theme } from '@models/Theme'; 8 | 9 | export interface DynamicContextProps { 10 | // Dynamic state values 11 | isDarkMode: boolean; 12 | isMobile: boolean; 13 | horizontalAll: boolean; 14 | textContentDensity: TextDensity; 15 | 16 | // Theme (can change with dark mode) 17 | memoizedTheme: Theme; 18 | 19 | // Interaction callbacks 20 | toggleDarkMode?: () => void; 21 | updateHorizontalAllCards?: (state: boolean) => void; 22 | updateTextContentDensity?: (value: TextDensity) => void; 23 | } 24 | 25 | export const DynamicContext = createContext({ 26 | isDarkMode: false, 27 | isMobile: false, 28 | horizontalAll: false, 29 | textContentDensity: 'HIGH', 30 | memoizedTheme: { 31 | primary: '#007FFF', 32 | secondary: '#ffdf00', 33 | cardBgColor: '#ffffff', 34 | cardDetailsBackGround: '#ffffff', 35 | cardDetailsColor: '#000', 36 | cardMediaBgColor: '#f5f5f5', 37 | cardSubtitleColor: '#000', 38 | cardTitleColor: '#007FFF', 39 | detailsColor: '#000', 40 | iconBackgroundColor: '#007FFF', 41 | nestedCardBgColor: '#f5f5f5', 42 | nestedCardDetailsBackGround: '#f5f5f5', 43 | nestedCardDetailsColor: '#000', 44 | nestedCardSubtitleColor: '#000', 45 | nestedCardTitleColor: '#000', 46 | titleColor: '#007FFF', 47 | titleColorActive: '#007FFF', 48 | toolbarBgColor: '#f5f5f5', 49 | toolbarBtnBgColor: '#ffffff', 50 | toolbarTextColor: '#000', 51 | timelineBgColor: '#f5f5f5', 52 | }, 53 | // Default no-op implementations to prevent fallback to legacy context 54 | toggleDarkMode: () => {}, 55 | updateHorizontalAllCards: () => {}, 56 | updateTextContentDensity: () => {}, 57 | }); 58 | -------------------------------------------------------------------------------- /src/components/contexts/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Context index file - exports all context-related functionality 3 | */ 4 | export { StableContext, type StableContextProps } from './StableContext'; 5 | export { DynamicContext, type DynamicContextProps } from './DynamicContext'; 6 | export { 7 | useStableContext, 8 | useDynamicContext, 9 | useGlobalContext, 10 | useTimelineContext, 11 | type CombinedContextProps, 12 | } from './hooks'; 13 | 14 | // Import and re-export to work around module resolution issue 15 | import { 16 | OptimizedContextProvider, 17 | type ContextProps, 18 | } from './OptimizedContextProvider'; 19 | export { OptimizedContextProvider }; 20 | export type { ContextProps }; 21 | -------------------------------------------------------------------------------- /src/components/contexts/legacy-types.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Legacy context types and default functions 3 | * This file contains only type definitions and default functions to avoid circular dependencies 4 | */ 5 | import { createContext } from 'react'; 6 | import { 7 | TimelineProps as PropsModel, 8 | TextDensity, 9 | } from '@models/TimelineModel'; 10 | 11 | export type LegacyContextProps = PropsModel & { 12 | isMobile?: boolean; 13 | toggleDarkMode?: () => void; 14 | updateHorizontalAllCards?: (state: boolean) => void; 15 | updateTextContentDensity?: (value: TextDensity) => void; 16 | }; 17 | 18 | export interface ButtonTexts { 19 | first?: string; 20 | last?: string; 21 | play?: string; 22 | stop?: string; 23 | previous?: string; 24 | next?: string; 25 | dark?: string; 26 | light?: string; 27 | timelinePoint?: string; 28 | searchPlaceholder?: string; 29 | searchAriaLabel?: string; 30 | clearSearch?: string; 31 | nextMatch?: string; 32 | previousMatch?: string; 33 | } 34 | 35 | // Legacy context for backward compatibility - exported without circular dependency 36 | export const LegacyGlobalContext = createContext({}); 37 | -------------------------------------------------------------------------------- /src/components/effects/__tests__/useSlideshow.test.ts: -------------------------------------------------------------------------------- 1 | // Test setup 2 | import { act, renderHook } from '@testing-library/react'; 3 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 4 | import { useSlideshow } from '../useSlideshow'; 5 | 6 | const slideItemDuration = 5000; 7 | const onElapsed = vi.fn(); 8 | 9 | let container = null; 10 | 11 | beforeEach(() => { 12 | container = document.createElement('div'); 13 | document.body.appendChild(container); 14 | }); 15 | 16 | afterEach(() => { 17 | document.body.removeChild(container); 18 | vi.clearAllMocks(); 19 | }); 20 | 21 | describe('useSlideshow', () => { 22 | it('should initialize properly', () => { 23 | const { result } = renderHook(() => 24 | useSlideshow( 25 | { current: container }, 26 | true, 27 | true, 28 | slideItemDuration, 29 | '1', 30 | onElapsed, 31 | ), 32 | ); 33 | 34 | expect(result.current.paused).toBe(false); 35 | expect(result.current.remainInterval).toBe(0); 36 | expect(result.current.startWidth).toBe(0); 37 | }); 38 | 39 | it('should set up timer correctly', async () => { 40 | const { result } = renderHook(() => 41 | useSlideshow( 42 | { current: container }, 43 | true, 44 | true, 45 | slideItemDuration, 46 | '1', 47 | onElapsed, 48 | ), 49 | ); 50 | 51 | expect(result.current.tryPause).toBeDefined(); 52 | expect(result.current.tryResume).toBeDefined(); 53 | expect(result.current.setStartWidth).toBeDefined(); 54 | expect(result.current.setupTimer).toBeDefined(); 55 | expect(result.current.startWidth).toBe(0); 56 | expect(result.current.paused).toBe(false); 57 | }); 58 | 59 | it('should pause slideshow', () => { 60 | const { result } = renderHook(() => 61 | useSlideshow( 62 | { current: container }, 63 | true, 64 | true, 65 | slideItemDuration, 66 | '1', 67 | onElapsed, 68 | ), 69 | ); 70 | 71 | expect(result.current.paused).toBe(false); 72 | 73 | act(() => { 74 | result.current.tryPause(); 75 | }); 76 | 77 | expect(result.current.paused).toBe(true); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/components/effects/useCloseClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { RefObject } from 'react'; 2 | import useOutsideClick from '../../hooks/useOutsideClick'; 3 | import useEscapeKey from '../../hooks/useEscapeKey'; 4 | 5 | /** 6 | * Custom hook that handles click outside and escape key events 7 | * @param el - Reference to the DOM element to watch for outside clicks 8 | * @param callback - Function to call when a click outside or escape key is detected 9 | */ 10 | export default function useCloseClickOutside( 11 | el: RefObject, 12 | callback: () => void, 13 | ) { 14 | useOutsideClick(el, callback); 15 | useEscapeKey(callback); 16 | } 17 | -------------------------------------------------------------------------------- /src/components/elements/list/__tests__/list.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | 3 | import { fireEvent } from '@testing-library/react'; 4 | import { getDefaultThemeOrDark } from '@utils/index'; 5 | import { customRender, providerProps } from 'src/components/common/test'; 6 | import { List } from '../list'; 7 | 8 | const items = [ 9 | { description: 'Description 1', id: '1', title: 'Item 1' }, 10 | { description: 'Description 2', id: '2', title: 'Item 2' }, 11 | { description: 'Description 3', id: '3', title: 'Item 3' }, 12 | ]; 13 | 14 | describe('List', () => { 15 | const theme = getDefaultThemeOrDark(false); 16 | 17 | it('should render the correct number of list items', () => { 18 | const { getAllByRole } = customRender( 19 | , 20 | { 21 | providerProps: { 22 | ...providerProps, 23 | }, 24 | }, 25 | ); 26 | const listItems = getAllByRole('listitem'); 27 | expect(listItems).toHaveLength(items.length); 28 | }); 29 | 30 | it('should call onClick when a list item is clicked', () => { 31 | const onClick = vi.fn(); 32 | const { getByText } = customRender( 33 | , 34 | { 35 | providerProps: { 36 | ...providerProps, 37 | }, 38 | }, 39 | ); 40 | const listItem = getByText('Item 1'); 41 | fireEvent.click(listItem); 42 | expect(onClick).toHaveBeenCalledTimes(1); 43 | }); 44 | 45 | it('should call onSelect when a list item is clicked and multiSelectable is true', () => { 46 | const onSelectItem = vi.fn(); 47 | const itemsWithOnSelect = items.map((item, index) => ({ 48 | ...item, 49 | onSelect: index === 0 ? onSelectItem : undefined, 50 | })); 51 | const { getByText } = customRender( 52 | , 53 | { 54 | providerProps: { 55 | ...providerProps, 56 | }, 57 | }, 58 | ); 59 | 60 | const listItem = getByText('Item 1'); 61 | fireEvent.click(listItem); 62 | expect(onSelectItem).toHaveBeenCalledTimes(1); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/elements/list/list.model.ts: -------------------------------------------------------------------------------- 1 | import { TimelineModel } from '@models/TimelineModel'; 2 | 3 | export type ListModel = { 4 | activeItemIndex?: number; 5 | items: ListItemModel[]; 6 | multiSelectable?: boolean; 7 | onClick?: (id?: string) => void; 8 | } & Pick; 9 | 10 | export type ListItemModel = { 11 | active?: boolean; 12 | description: string; 13 | id: string; 14 | onClick?: (id: string) => void; 15 | onSelect?: () => void; 16 | selectable?: boolean; 17 | selected?: boolean; 18 | title: string; 19 | } & Pick; 20 | -------------------------------------------------------------------------------- /src/components/elements/popover/popover.model.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@models/Theme'; 2 | import { ReactNode } from 'react'; 3 | 4 | export type PopOverModel = { 5 | $isMobile?: boolean; 6 | children: ReactNode | ReactNode[]; 7 | icon?: ReactNode; 8 | isDarkMode?: boolean; 9 | placeholder?: string; 10 | position: 'top' | 'bottom'; 11 | theme?: Theme; 12 | width?: number; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/icons/arrow-down.tsx: -------------------------------------------------------------------------------- 1 | const ArrowDownIcon = () => ( 2 | 7 | 8 | 9 | ) 10 | export default ArrowDownIcon 11 | -------------------------------------------------------------------------------- /src/components/icons/check.tsx: -------------------------------------------------------------------------------- 1 | const CheckIcon = () => ( 2 | 11 | 12 | 13 | ) 14 | export default CheckIcon 15 | -------------------------------------------------------------------------------- /src/components/icons/chev-down.tsx: -------------------------------------------------------------------------------- 1 | const ChevDownIcon = () => ( 2 | 11 | 12 | 13 | ) 14 | export default ChevDownIcon 15 | -------------------------------------------------------------------------------- /src/components/icons/chev-left.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ChevronLeft: React.FunctionComponent = () => ( 4 | 16 | 17 | 18 | ); 19 | 20 | export default ChevronLeft; 21 | -------------------------------------------------------------------------------- /src/components/icons/chev-right.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ChevronRightIcon: React.FunctionComponent = () => ( 4 | 16 | 17 | 18 | ); 19 | 20 | export default ChevronRightIcon; 21 | -------------------------------------------------------------------------------- /src/components/icons/chev-up.tsx: -------------------------------------------------------------------------------- 1 | const ChevUpIcon = () => ( 2 | 10 | 11 | 12 | ) 13 | export default ChevUpIcon 14 | -------------------------------------------------------------------------------- /src/components/icons/chevs-left.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ChevronLeft: React.FunctionComponent = () => ( 4 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default ChevronLeft; 22 | -------------------------------------------------------------------------------- /src/components/icons/chevs-right.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ChevronRightIcon: React.FunctionComponent = () => ( 4 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default ChevronRightIcon; 22 | -------------------------------------------------------------------------------- /src/components/icons/close.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | function SvgComponent() { 4 | return ( 5 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default SvgComponent 23 | -------------------------------------------------------------------------------- /src/components/icons/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as ArrowDownIcon } from './arrow-down'; 2 | export { default as CheckIcon } from './check'; 3 | export { default as ChevronDown } from './chev-down'; 4 | export { default as ChevronLeft } from './chev-left'; 5 | export { default as ChevronRight } from './chev-right'; 6 | export { default as ChevronUp } from './chev-up'; 7 | export { default as CloseIcon } from './close'; 8 | export { default as LayoutIcon } from './layout'; 9 | export { default as MaximizeIcon } from './maximize'; 10 | export { default as MinimizeIcon } from './minimize'; 11 | export { default as MinusIcon } from './minus'; 12 | export { default as MoonIcon } from './moon'; 13 | export { default as PlusIcon } from './plus'; 14 | export { default as StopIcon } from "./stop"; 15 | export { default as SunIcon } from './sun'; 16 | export { default as TextIcon } from './text'; 17 | export {default as ParaIcon} from './para'; 18 | -------------------------------------------------------------------------------- /src/components/icons/layout.tsx: -------------------------------------------------------------------------------- 1 | const LayoutIcon = (props) => ( 2 | 9 | 10 | 11 | ) 12 | export default LayoutIcon 13 | -------------------------------------------------------------------------------- /src/components/icons/maximize.tsx: -------------------------------------------------------------------------------- 1 | const SvgComponent = () => ( 2 | 13 | 14 | 15 | ) 16 | 17 | export default SvgComponent 18 | -------------------------------------------------------------------------------- /src/components/icons/menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | function SvgComponent() { 4 | return ( 5 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default SvgComponent 23 | -------------------------------------------------------------------------------- /src/components/icons/minimize.tsx: -------------------------------------------------------------------------------- 1 | const SvgComponent = () => ( 2 | 12 | 13 | 14 | ) 15 | 16 | export default SvgComponent 17 | -------------------------------------------------------------------------------- /src/components/icons/minus.tsx: -------------------------------------------------------------------------------- 1 | 2 | const SvgComponent = () => ( 3 | 12 | 13 | 14 | ) 15 | 16 | export default SvgComponent 17 | -------------------------------------------------------------------------------- /src/components/icons/moon.tsx: -------------------------------------------------------------------------------- 1 | 2 | const SvgComponent = () => ( 3 | 12 | 13 | 14 | ) 15 | 16 | export default SvgComponent 17 | -------------------------------------------------------------------------------- /src/components/icons/para.tsx: -------------------------------------------------------------------------------- 1 | const SvgComponent = () => ( 2 | 7 | 8 | 9 | ) 10 | export default SvgComponent 11 | -------------------------------------------------------------------------------- /src/components/icons/plus.tsx: -------------------------------------------------------------------------------- 1 | 2 | const SvgComponent = () => ( 3 | 12 | 13 | 14 | ) 15 | 16 | export default SvgComponent 17 | -------------------------------------------------------------------------------- /src/components/icons/replay-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ReplayIcon: React.FunctionComponent = () => ( 4 | 15 | 16 | 17 | ); 18 | 19 | export default ReplayIcon; 20 | -------------------------------------------------------------------------------- /src/components/icons/stop.tsx: -------------------------------------------------------------------------------- 1 | 2 | const SvgComponent = () => ( 3 | 12 | 13 | 14 | 15 | ) 16 | 17 | export default SvgComponent 18 | -------------------------------------------------------------------------------- /src/components/icons/sun.tsx: -------------------------------------------------------------------------------- 1 | 2 | const SvgComponent = () => ( 3 | 13 | 14 | 15 | 16 | ) 17 | 18 | export default SvgComponent 19 | -------------------------------------------------------------------------------- /src/components/icons/text.tsx: -------------------------------------------------------------------------------- 1 | const TextIcon = () => ( 2 | 7 | 8 | 9 | ) 10 | export default TextIcon 11 | -------------------------------------------------------------------------------- /src/components/icons/triangle-right.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | function SvgComponent() { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } 10 | 11 | export default SvgComponent 12 | -------------------------------------------------------------------------------- /src/components/timeline-elements/memoized/details-text-memo.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useCallback } from 'react'; 2 | import { useBackground } from '../../../hooks/useBackground'; 3 | import { DetailsTextWrapper } from '../timeline-card-media/timeline-card-media.styles'; 4 | import { DetailsTextMemoModel } from './memoized-model'; 5 | 6 | const useMeasureHeight = (onRender?: (height: number) => void) => { 7 | return useCallback( 8 | (node: HTMLDivElement | null) => { 9 | if (node && onRender) { 10 | requestAnimationFrame(() => onRender(node.clientHeight)); 11 | } 12 | }, 13 | [onRender], 14 | ); 15 | }; 16 | 17 | const arePropsEqual = ( 18 | prev: DetailsTextMemoModel, 19 | next: DetailsTextMemoModel, 20 | ): boolean => { 21 | return ( 22 | prev.height === next.height && 23 | prev.show === next.show && 24 | prev.expand === next.expand && 25 | prev.theme?.cardDetailsBackGround === next.theme?.cardDetailsBackGround 26 | ); 27 | }; 28 | 29 | const DetailsTextMemo = memo( 30 | ({ theme, show, expand, textOverlay, text: Text, onRender }) => { 31 | const background = useBackground(theme?.cardDetailsBackGround); 32 | const measureRef = useMeasureHeight(onRender); 33 | 34 | if (!textOverlay) return null; 35 | 36 | return ( 37 | 44 | 45 | 46 | ); 47 | }, 48 | arePropsEqual, 49 | ); 50 | 51 | DetailsTextMemo.displayName = 'DetailsText'; 52 | 53 | export { DetailsTextMemo }; 54 | -------------------------------------------------------------------------------- /src/components/timeline-elements/memoized/expand-button-memo.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from 'react'; 2 | import { MaximizeIcon, MinimizeIcon } from '../../icons'; 3 | import { ExpandButton } from '../timeline-card-media/timeline-card-media-buttons'; 4 | import { ExpandButtonModel } from './memoized-model'; 5 | 6 | /** 7 | * Renders a button to expand or collapse timeline card content. 8 | * @param {ExpandButtonModel} props - The expand button properties 9 | * @returns {JSX.Element | null} The expand/collapse button 10 | */ 11 | const ExpandButtonMemo = memo( 12 | ({ theme, expanded, onExpand, textOverlay }: ExpandButtonModel) => { 13 | const label = useMemo(() => { 14 | return expanded ? 'Minimize' : 'Maximize'; 15 | }, [expanded]); 16 | 17 | return textOverlay ? ( 18 | ev.key === 'Enter' && onExpand?.(ev)} 21 | theme={theme} 22 | aria-expanded={expanded} 23 | tabIndex={0} 24 | aria-label={label} 25 | title={label} 26 | > 27 | {expanded ? : } 28 | 29 | ) : null; 30 | }, 31 | (prev, next) => prev.expanded === next.expanded, 32 | ); 33 | 34 | ExpandButtonMemo.displayName = 'Expand Button'; 35 | 36 | export { ExpandButtonMemo }; 37 | -------------------------------------------------------------------------------- /src/components/timeline-elements/memoized/memoized-model.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '../../../models/Theme'; 2 | import React, { ReactNode } from 'react'; 3 | 4 | /** 5 | * Common properties shared by multiple interfaces. 6 | */ 7 | type common = { 8 | classString?: string; // CSS class string 9 | color?: string; // Color value 10 | dir?: string; // Text direction 11 | fontSize?: string; // Font size 12 | $padding?: boolean; // Whether to apply padding 13 | theme?: Theme; // Theme object 14 | }; 15 | 16 | /** 17 | * Interface for the Title component. 18 | */ 19 | export interface Title extends common { 20 | active?: boolean; // Whether the title is active 21 | title?: string | ReactNode; // Title text or ReactNode 22 | url?: string; // URL for the title 23 | } 24 | 25 | /** 26 | * Interface for the Content component. 27 | */ 28 | export interface Content extends common { 29 | content?: string | ReactNode; // Content text or ReactNode 30 | padding?: boolean; // For backward compatibility 31 | } 32 | 33 | /** 34 | * Type for the ExpandButtonModel. 35 | */ 36 | export type ExpandButtonModel = { 37 | expanded?: boolean; // Whether the button is expanded 38 | onExpand?: (ev: React.PointerEvent | React.KeyboardEvent) => void; // Event handler for expand action 39 | textOverlay?: boolean; // Whether to overlay text on the button 40 | } & Pick; 41 | 42 | /** 43 | * Type for the ShowHideTextButtonModel. 44 | */ 45 | export type ShowHideTextButtonModel = { 46 | onToggle: (ev: React.PointerEvent | React.KeyboardEvent) => void; // Event handler for toggle action 47 | show?: boolean; // Whether to show the button 48 | textOverlay?: boolean; // Whether to overlay text on the button 49 | } & Pick; 50 | 51 | /** 52 | * Type for the DetailsTextMemoModel. 53 | */ 54 | export type DetailsTextMemoModel = { 55 | theme?: Theme; // Theme object 56 | show: boolean; // Whether to show the details text 57 | expand: boolean; // Whether to expand the details text 58 | textOverlay: boolean; // Whether to overlay text on the details text 59 | text: React.FC; // Text component for the details text 60 | height?: number; // Height of the details text 61 | onRender?: (height: number) => void; // Callback function for rendering the details text 62 | }; 63 | 64 | /** 65 | * Type for the TextContentMemoModel. 66 | */ 67 | export type TextContentMemoModel = Title & 68 | Content & 69 | ExpandButtonModel & 70 | ShowHideTextButtonModel & 71 | DetailsTextMemoModel; 72 | 73 | /** 74 | * Type for the CardMediaHeaderMemoModel. 75 | */ 76 | export type CardMediaHeaderMemoModel = Title & Content; 77 | -------------------------------------------------------------------------------- /src/components/timeline-elements/memoized/show-hide-button.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from 'react'; 2 | import { MinusIcon, PlusIcon } from '../../icons'; 3 | import { ShowHideTextButton } from '../timeline-card-media/timeline-card-media-buttons'; 4 | import { ShowHideTextButtonModel } from './memoized-model'; 5 | 6 | /** 7 | * Renders a button to toggle showing or hiding text content. 8 | * @param {ShowHideTextButtonModel} props - Button properties 9 | * @returns {JSX.Element | null} The toggle button 10 | */ 11 | const ShowOrHideTextButtonMemo = memo( 12 | ({ textOverlay, onToggle, theme, show }: ShowHideTextButtonModel) => { 13 | const label = useMemo(() => { 14 | return show ? 'Hide Text' : 'Show Text'; 15 | }, [show]); 16 | 17 | return textOverlay ? ( 18 | ev.key === 'Enter' && onToggle?.(ev)} 23 | aria-label={label} 24 | title={label} 25 | > 26 | {show ? : } 27 | 28 | ) : null; 29 | }, 30 | ); 31 | 32 | ShowOrHideTextButtonMemo.displayName = 'Show Hide Text Button'; 33 | 34 | export { ShowOrHideTextButtonMemo }; 35 | -------------------------------------------------------------------------------- /src/components/timeline-elements/memoized/subtitle-memo.tsx: -------------------------------------------------------------------------------- 1 | import cls from 'classnames'; 2 | import React, { useContext } from 'react'; 3 | import { CardSubTitleSemantic } from '../timeline-card-content/timeline-card-content.styles'; 4 | import { Content } from './memoized-model'; 5 | import { GlobalContext } from '../../GlobalContext'; 6 | 7 | /** 8 | * Renders the subtitle content for the timeline card using configurable semantic tags. 9 | * @param {Content} props - Subtitle properties 10 | * @returns {JSX.Element | null} The rendered subtitle 11 | */ 12 | const SubTitleMemo = React.memo( 13 | ({ content, color, dir, theme, fontSize, classString, padding }: Content) => { 14 | const { semanticTags } = useContext(GlobalContext); 15 | 16 | return content ? ( 17 | 26 | {content} 27 | 28 | ) : null; 29 | }, 30 | (prev, next) => 31 | prev.theme?.cardSubtitleColor === next.theme?.cardSubtitleColor, 32 | ); 33 | 34 | SubTitleMemo.displayName = 'Timeline Content'; 35 | 36 | export { SubTitleMemo }; 37 | -------------------------------------------------------------------------------- /src/components/timeline-elements/memoized/title-memo.tsx: -------------------------------------------------------------------------------- 1 | import cls from 'classnames'; 2 | import { 3 | CardTitleAnchor, 4 | CardTitleSemantic, 5 | } from '../timeline-card-content/timeline-card-content.styles'; 6 | import { Title } from './memoized-model'; 7 | import React, { useContext } from 'react'; 8 | import { GlobalContext } from '../../GlobalContext'; 9 | 10 | /** 11 | * Renders the timeline's title with optional link using configurable semantic tags. 12 | * @param {Title} props - Title properties 13 | * @returns {JSX.Element | null} The rendered title 14 | */ 15 | const TitleMemoComponent = ({ 16 | title, 17 | url, 18 | theme, 19 | color, 20 | dir, 21 | active, 22 | fontSize = '1rem', 23 | classString = '', 24 | }: Title) => { 25 | const { semanticTags } = useContext(GlobalContext); 26 | 27 | return title ? ( 28 | 37 | {url ? ( 38 | 39 | {title} 40 | 41 | ) : ( 42 | title 43 | )} 44 | 45 | ) : null; 46 | }; 47 | 48 | export const TitleMemo = React.memo(TitleMemoComponent); 49 | 50 | TitleMemo.displayName = 'Timeline Title'; 51 | -------------------------------------------------------------------------------- /src/components/timeline-elements/nested-timeline-renderer/nested-timeline-renderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TimelineModel } from '@models/TimelineModel'; 3 | 4 | interface NestedTimelineRendererProps { 5 | items: any[]; 6 | nestedCardHeight?: number; 7 | mode?: string; 8 | isChild?: boolean; 9 | } 10 | 11 | // This component will be used to dynamically load the Timeline component 12 | // to avoid circular dependency issues 13 | const NestedTimelineRenderer: React.FC = ({ 14 | items, 15 | nestedCardHeight, 16 | mode = 'HORIZONTAL', 17 | isChild, 18 | }) => { 19 | const [TimelineComponent, setTimelineComponent] = 20 | React.useState | null>(null); 21 | 22 | // Dynamically import the Timeline component only when needed 23 | React.useEffect(() => { 24 | import('../../timeline/timeline').then((module) => { 25 | setTimelineComponent(() => module.default); 26 | }); 27 | }, []); 28 | 29 | if (!TimelineComponent) { 30 | return
Loading nested timeline...
; 31 | } 32 | 33 | return ( 34 | 40 | ); 41 | }; 42 | 43 | export default NestedTimelineRenderer; 44 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/__tests__/__snapshots__/content-footer.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ContentFooter > should match snapshot 1`] = `
`; 4 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/__tests__/__snapshots__/content-header.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Content Header > should match the snapshot 1`] = ` 4 |
5 |
8 | 12 | title 13 | 14 | 17 | content 18 | 19 |
20 |
21 | `; 22 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/__tests__/content-header.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest'; 2 | import { customRender } from '../../../common/test'; 3 | import { providerProps } from '../../../common/test/index'; 4 | import { ContentHeader } from '../content-header'; 5 | 6 | describe('Content Header', () => { 7 | // should render the title and subtitle of the card 8 | it('should render the title and subtitle of the card', () => { 9 | const { getByText } = customRender( 10 | , 11 | { 12 | providerProps: { 13 | ...providerProps, 14 | media: null, 15 | }, 16 | }, 17 | ); 18 | 19 | expect(getByText('title')).toBeInTheDocument(); 20 | expect(getByText('content')).toBeInTheDocument(); 21 | }); 22 | 23 | // should match the snapshot 24 | it('should match the snapshot', () => { 25 | const { container } = customRender( 26 | , 27 | { 28 | providerProps, 29 | }, 30 | ); 31 | 32 | expect(container).toMatchSnapshot(); 33 | }); 34 | 35 | // should render the link if url is provided 36 | it('should render the link if url is provided', () => { 37 | const { getByText, getByRole } = customRender( 38 | , 43 | { 44 | providerProps, 45 | }, 46 | ); 47 | 48 | expect(getByText('title')).toBeInTheDocument(); 49 | expect(getByRole('link')).toHaveAttribute('href', 'http://www.google.com'); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/__tests__/text_or_content.test.tsx: -------------------------------------------------------------------------------- 1 | // Imports 2 | import { getDefaultClassNames } from '@utils/index'; 3 | import { customRender, providerProps } from 'src/components/common/test'; 4 | import { getTextOrContent } from '../text-or-content'; 5 | 6 | describe('getTextOrContent', () => { 7 | const classNames = getDefaultClassNames(); 8 | 9 | it('renders TimelineContentDetails when detailedText is string', () => { 10 | const TextContent = getTextOrContent({ 11 | detailedText: 'Hello World', 12 | }); 13 | 14 | const { getByText } = customRender(, { 15 | providerProps: { 16 | ...providerProps, 17 | }, 18 | }); 19 | expect(getByText('Hello World')).toBeInTheDocument(); 20 | }); 21 | 22 | it('renders TimelineContentDetails with xss when parseDetailsAsHTML is true', () => { 23 | const TextContent = getTextOrContent({ 24 | detailedText: 'Hello', 25 | }); 26 | 27 | const { container } = customRender(, { 28 | providerProps: { 29 | ...providerProps, 30 | parseDetailsAsHTML: true, 31 | }, 32 | }); 33 | 34 | expect(container.firstChild).toHaveTextContent('Hello'); // xss sanitized 35 | }); 36 | 37 | it('renders TimelineSubContent when detailedText is array', () => { 38 | const TextContent = getTextOrContent({ 39 | detailedText: ['Line 1', 'Line 2'], 40 | }); 41 | 42 | const { getAllByText } = customRender(, { 43 | providerProps: { 44 | ...providerProps, 45 | }, 46 | }); 47 | expect(getAllByText('Line 1')[0]).toBeInTheDocument(); 48 | expect(getAllByText('Line 2')[0]).toBeInTheDocument(); 49 | }); 50 | 51 | it('passes showMore prop to TimelineContentDetails', () => { 52 | const TextContent = getTextOrContent({ 53 | detailedText: 'Text', 54 | showMore: true, 55 | }); 56 | 57 | const { getByText } = customRender(, { 58 | providerProps: { 59 | ...providerProps, 60 | }, 61 | }); 62 | expect(getByText('Text')).toHaveClass('active'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/card-animations.styles.ts: -------------------------------------------------------------------------------- 1 | import { keyframes } from 'styled-components'; 2 | 3 | export const reveal = keyframes` 4 | 0% { 5 | opacity: 0; 6 | transform: scale(0.95); 7 | } 8 | 100% { 9 | opacity: 1; 10 | transform: scale(1); 11 | } 12 | `; 13 | 14 | export const slideInFromTop = keyframes` 15 | 0% { 16 | opacity: 0; 17 | transform: translateY(-50%); 18 | } 19 | 100% { 20 | opacity: 1; 21 | transform: translateY(0); 22 | } 23 | `; 24 | 25 | export const slideInFromLeft = keyframes` 26 | 0% { 27 | opacity: 0; 28 | transform: translateX(-50%); 29 | } 30 | 100% { 31 | opacity: 1; 32 | transform: translateX(0); 33 | } 34 | `; 35 | 36 | export const slideFromRight = keyframes` 37 | 0% { 38 | opacity: 0; 39 | transform: translateX(50%); 40 | } 41 | 100% { 42 | opacity: 1; 43 | transform: translateX(0); 44 | } 45 | `; 46 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/details-text.model.ts: -------------------------------------------------------------------------------- 1 | import { TimelineContentModel } from '@models/TimelineContentModel'; 2 | import { ReactNode } from 'react'; 3 | 4 | export type DetailsTextProps = { 5 | cardActualHeight?: number; 6 | contentDetailsClass?: string; 7 | customContent?: ReactNode; 8 | detailedText: TimelineContentModel['detailedText']; 9 | detailsHeight?: number; 10 | gradientColor?: string; 11 | showMore?: boolean; 12 | timelineContent: TimelineContentModel['timelineContent']; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/details-text.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useContext } from 'react'; 2 | import { GlobalContext } from '../../GlobalContext'; 3 | import { DetailsTextProps } from './details-text.model'; 4 | import { getTextOrContent } from './text-or-content'; 5 | import { TimelineContentDetailsWrapper } from './timeline-card-content.styles'; 6 | 7 | const DetailsText = forwardRef( 8 | (prop, ref) => { 9 | const { 10 | showMore, 11 | cardActualHeight, 12 | detailsHeight, 13 | gradientColor, 14 | customContent, 15 | timelineContent, 16 | detailedText, 17 | contentDetailsClass, 18 | } = prop; 19 | 20 | const { 21 | useReadMore, 22 | borderLessCards, 23 | contentDetailsHeight, 24 | textOverlay, 25 | theme, 26 | } = useContext(GlobalContext); 27 | 28 | const TextContent = getTextOrContent({ 29 | detailedText, 30 | showMore, 31 | theme, 32 | timelineContent, 33 | }); 34 | 35 | return ( 36 | <> 37 | {/* detailed text */} 38 | 53 | {customContent ?? ( 54 | 57 | )} 58 | 59 | 60 | ); 61 | }, 62 | ); 63 | 64 | DetailsText.displayName = 'Details Text'; 65 | 66 | export { DetailsText }; 67 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-content/header-footer.model.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@models/Theme'; 2 | import { TimelineContentModel } from '@models/TimelineContentModel'; 3 | 4 | export type ContentHeaderProps = Pick< 5 | TimelineContentModel, 6 | 'theme' | 'url' | 'title' | 'media' | 'content' | 'cardTitle' 7 | >; 8 | 9 | export type ContentFooterProps = { 10 | canShow: boolean; 11 | isNested?: boolean; 12 | onExpand: () => void; 13 | remainInterval: number; 14 | showMore: boolean; 15 | showReadMore?: boolean | ''; 16 | startWidth: number; 17 | textContentIsLarge: boolean; 18 | theme?: Theme; 19 | triangleDir?: string; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/timeline-elements/timeline-card-media/__tests__/__snapshots__/timeline-card-media.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Timeline Card media > should match the snapshot ( IMAGE ) 1`] = ` 4 |
5 |
9 |
13 | Image 23 |
24 |
25 |
29 |
32 | 35 | This is another test 36 | 37 |
38 |
39 | `; 40 | 41 | exports[`Timeline Card media > should match the snapshot ( VIDEO ) 1`] = ` 42 |
43 |
47 |