summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json6
-rw-r--r--.storybook/main.js55
-rw-r--r--.storybook/manager.js6
-rw-r--r--.storybook/overrides/docs-container.js36
-rw-r--r--.storybook/preview.js74
-rw-r--r--.storybook/themes/common.js8
-rw-r--r--.storybook/themes/dark.js37
-rw-r--r--.storybook/themes/light.js36
-rw-r--r--README.md17
-rw-r--r--__tests__/jest/components/Branding.test.tsx25
-rw-r--r--__tests__/jest/components/Copyright.test.tsx14
-rw-r--r--__tests__/jest/components/Header.test.tsx13
-rw-r--r--__tests__/utils/test-utils.tsx3
-rw-r--r--jest.setup.js2
-rw-r--r--mdx.d.ts10
-rw-r--r--next.config.js4
-rw-r--r--package.json24
-rw-r--r--src/components/Branding/Branding.module.scss169
-rw-r--r--src/components/Branding/Branding.tsx107
-rw-r--r--src/components/Breadcrumb/Breadcrumb.tsx155
-rw-r--r--src/components/Buttons/Button/Button.tsx35
-rw-r--r--src/components/Buttons/ButtonHelp/ButtonHelp.module.scss52
-rw-r--r--src/components/Buttons/ButtonHelp/ButtonHelp.tsx42
-rw-r--r--src/components/Buttons/ButtonLink/ButtonLink.tsx30
-rw-r--r--src/components/Buttons/ButtonSubmit/ButtonSubmit.tsx22
-rw-r--r--src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx72
-rw-r--r--src/components/Buttons/Buttons.module.scss289
-rw-r--r--src/components/Buttons/index.tsx7
-rw-r--r--src/components/Comment/Comment.module.scss99
-rw-r--r--src/components/Comment/Comment.tsx200
-rw-r--r--src/components/CommentForm/CommentForm.module.scss25
-rw-r--r--src/components/CommentForm/CommentForm.tsx240
-rw-r--r--src/components/CommentsList/CommentsList.module.scss14
-rw-r--r--src/components/CommentsList/CommentsList.tsx69
-rw-r--r--src/components/ContactForm/ContactForm.module.scss21
-rw-r--r--src/components/ContactForm/ContactForm.tsx180
-rw-r--r--src/components/Copyright/Copyright.tsx17
-rw-r--r--src/components/Footer/Footer.module.scss90
-rw-r--r--src/components/Footer/Footer.tsx54
-rw-r--r--src/components/FooterNav/FooterNav.module.scss20
-rw-r--r--src/components/FooterNav/FooterNav.tsx45
-rw-r--r--src/components/FormElements/Field/Field.module.scss53
-rw-r--r--src/components/FormElements/Field/Field.tsx106
-rw-r--r--src/components/FormElements/Form/Form.module.scss37
-rw-r--r--src/components/FormElements/Form/Form.tsx27
-rw-r--r--src/components/FormElements/FormItem/FormItem.module.scss4
-rw-r--r--src/components/FormElements/FormItem/FormItem.tsx7
-rw-r--r--src/components/FormElements/Label/Label.module.scss22
-rw-r--r--src/components/FormElements/Label/Label.tsx24
-rw-r--r--src/components/FormElements/Toggle/Toggle.tsx46
-rw-r--r--src/components/FormElements/index.tsx7
-rw-r--r--src/components/Header/Header.module.scss22
-rw-r--r--src/components/Header/Header.tsx16
-rw-r--r--src/components/Icons/Arrow/Arrow.module.scss7
-rw-r--r--src/components/Icons/Hamburger/Hamburger.tsx10
-rw-r--r--src/components/Icons/index.tsx29
-rw-r--r--src/components/Layouts/Layout.module.scss20
-rw-r--r--src/components/Layouts/Layout.tsx155
-rw-r--r--src/components/MDX/CodeBlock/CodeBlock.tsx115
-rw-r--r--src/components/MDX/Gallery/Gallery.tsx22
-rw-r--r--src/components/MDX/Link/Link.tsx23
-rw-r--r--src/components/MDX/ResponsiveImage/ResponsiveImage.module.scss50
-rw-r--r--src/components/MDX/ResponsiveImage/ResponsiveImage.tsx40
-rw-r--r--src/components/MDX/index.tsx6
-rw-r--r--src/components/Main/Main.module.scss7
-rw-r--r--src/components/Main/Main.tsx12
-rw-r--r--src/components/MainNav/MainNav.module.scss242
-rw-r--r--src/components/MainNav/MainNav.tsx155
-rw-r--r--src/components/MetaItems/Author/Author.tsx21
-rw-r--r--src/components/MetaItems/CommentsCount/CommentsCount.tsx43
-rw-r--r--src/components/MetaItems/Dates/Dates.tsx58
-rw-r--r--src/components/MetaItems/MetaItem/MetaItem.module.scss18
-rw-r--r--src/components/MetaItems/MetaItem/MetaItem.tsx36
-rw-r--r--src/components/MetaItems/PostsCount/PostsCount.tsx29
-rw-r--r--src/components/MetaItems/ReadingTime/ReadingTime.tsx59
-rw-r--r--src/components/MetaItems/Thematics/Thematics.tsx43
-rw-r--r--src/components/MetaItems/Topics/Topics.tsx37
-rw-r--r--src/components/MetaItems/Website/Website.tsx21
-rw-r--r--src/components/MetaItems/index.tsx21
-rw-r--r--src/components/Notice/Notice.tsx21
-rw-r--r--src/components/Pagination/Pagination.module.scss92
-rw-r--r--src/components/Pagination/Pagination.tsx136
-rw-r--r--src/components/PaginationCursor/PaginationCursor.tsx43
-rw-r--r--src/components/PostFooter/PostFooter.module.scss18
-rw-r--r--src/components/PostFooter/PostFooter.tsx53
-rw-r--r--src/components/PostHeader/PostHeader.tsx57
-rw-r--r--src/components/PostMeta/PostMeta.module.scss31
-rw-r--r--src/components/PostMeta/PostMeta.tsx67
-rw-r--r--src/components/PostPreview/PostPreview.module.scss105
-rw-r--r--src/components/PostPreview/PostPreview.tsx120
-rw-r--r--src/components/PostsList/PostsList.module.scss51
-rw-r--r--src/components/PostsList/PostsList.tsx110
-rw-r--r--src/components/ProjectPreview/ProjectPreview.module.scss98
-rw-r--r--src/components/ProjectPreview/ProjectPreview.tsx73
-rw-r--r--src/components/ProjectSummary/ProjectSummary.module.scss73
-rw-r--r--src/components/ProjectSummary/ProjectSummary.tsx178
-rw-r--r--src/components/ProjectsList/ProjectsList.module.scss25
-rw-r--r--src/components/ProjectsList/ProjectsList.tsx21
-rw-r--r--src/components/SearchForm/SearchForm.module.scss6
-rw-r--r--src/components/SearchForm/SearchForm.tsx70
-rw-r--r--src/components/Settings/AckeeSelect/AckeeSelect.tsx96
-rw-r--r--src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx50
-rw-r--r--src/components/Settings/ReduceMotion/ReduceMotion.tsx48
-rw-r--r--src/components/Settings/Settings.module.scss17
-rw-r--r--src/components/Settings/Settings.tsx30
-rw-r--r--src/components/Settings/ThemeToggle/ThemeToggle.tsx41
-rw-r--r--src/components/Sidebar/Sidebar.module.scss43
-rw-r--r--src/components/Sidebar/Sidebar.tsx40
-rw-r--r--src/components/Toolbar/Toolbar.module.scss114
-rw-r--r--src/components/Toolbar/Toolbar.tsx162
-rw-r--r--src/components/Tooltip/Tooltip.module.scss120
-rw-r--r--src/components/Tooltip/Tooltip.tsx59
-rw-r--r--src/components/WidgetParts/ExpandableWidget/ExpandableWidget.module.scss146
-rw-r--r--src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx61
-rw-r--r--src/components/WidgetParts/List/List.module.scss49
-rw-r--r--src/components/WidgetParts/List/List.tsx7
-rw-r--r--src/components/WidgetParts/OrderedList/OrderedList.module.scss66
-rw-r--r--src/components/WidgetParts/OrderedList/OrderedList.tsx7
-rw-r--r--src/components/WidgetParts/index.tsx5
-rw-r--r--src/components/Widgets/CVPreview/CVPreview.module.scss6
-rw-r--r--src/components/Widgets/CVPreview/CVPreview.tsx45
-rw-r--r--src/components/Widgets/RecentPosts/RecentPosts.module.scss109
-rw-r--r--src/components/Widgets/RecentPosts/RecentPosts.tsx78
-rw-r--r--src/components/Widgets/RelatedThematics/RelatedThematics.tsx41
-rw-r--r--src/components/Widgets/RelatedTopics/RelatedTopics.tsx41
-rw-r--r--src/components/Widgets/Sharing/Sharing.tsx238
-rw-r--r--src/components/Widgets/SocialMedia/SocialMedia.tsx113
-rw-r--r--src/components/Widgets/ThematicsList/ThematicsList.tsx76
-rw-r--r--src/components/Widgets/ToC/ToC.tsx55
-rw-r--r--src/components/Widgets/TopicsList/TopicsList.tsx76
-rw-r--r--src/components/Widgets/index.tsx21
-rw-r--r--src/components/atoms/buttons/button-link.stories.tsx139
-rw-r--r--src/components/atoms/buttons/button-link.test.tsx9
-rw-r--r--src/components/atoms/buttons/button-link.tsx73
-rw-r--r--src/components/atoms/buttons/button.stories.tsx172
-rw-r--r--src/components/atoms/buttons/button.test.tsx18
-rw-r--r--src/components/atoms/buttons/button.tsx77
-rw-r--r--src/components/atoms/buttons/buttons.module.scss177
-rw-r--r--src/components/atoms/forms/checkbox.stories.tsx102
-rw-r--r--src/components/atoms/forms/checkbox.test.tsx28
-rw-r--r--src/components/atoms/forms/checkbox.tsx46
-rw-r--r--src/components/atoms/forms/field.stories.tsx257
-rw-r--r--src/components/atoms/forms/field.test.tsx30
-rw-r--r--src/components/atoms/forms/field.tsx111
-rw-r--r--src/components/atoms/forms/form.test.tsx13
-rw-r--r--src/components/atoms/forms/form.tsx76
-rw-r--r--src/components/atoms/forms/forms.module.scss53
-rw-r--r--src/components/atoms/forms/label.module.scss17
-rw-r--r--src/components/atoms/forms/label.stories.tsx104
-rw-r--r--src/components/atoms/forms/label.test.tsx9
-rw-r--r--src/components/atoms/forms/label.tsx53
-rw-r--r--src/components/atoms/forms/select.stories.tsx151
-rw-r--r--src/components/atoms/forms/select.test.tsx30
-rw-r--r--src/components/atoms/forms/select.tsx99
-rw-r--r--src/components/atoms/headings/heading.module.scss69
-rw-r--r--src/components/atoms/headings/heading.stories.tsx160
-rw-r--r--src/components/atoms/headings/heading.test.tsx56
-rw-r--r--src/components/atoms/headings/heading.tsx94
-rw-r--r--src/components/atoms/icons/arrow.module.scss16
-rw-r--r--src/components/atoms/icons/arrow.stories.tsx48
-rw-r--r--src/components/atoms/icons/arrow.test.tsx9
-rw-r--r--src/components/atoms/icons/arrow.tsx (renamed from src/components/Icons/Arrow/Arrow.tsx)36
-rw-r--r--src/components/atoms/icons/career.module.scss (renamed from src/components/Icons/CV/CV.module.scss)1
-rw-r--r--src/components/atoms/icons/career.stories.tsx34
-rw-r--r--src/components/atoms/icons/career.test.tsx9
-rw-r--r--src/components/atoms/icons/career.tsx (renamed from src/components/Icons/CV/CV.tsx)21
-rw-r--r--src/components/atoms/icons/cc-by-sa.module.scss7
-rw-r--r--src/components/atoms/icons/cc-by-sa.stories.tsx34
-rw-r--r--src/components/atoms/icons/cc-by-sa.test.tsx9
-rw-r--r--src/components/atoms/icons/cc-by-sa.tsx (renamed from src/components/Icons/Copyright/Copyright.tsx)32
-rw-r--r--src/components/atoms/icons/close.module.scss (renamed from src/components/Icons/Close/Close.module.scss)1
-rw-r--r--src/components/atoms/icons/close.stories.tsx34
-rw-r--r--src/components/atoms/icons/close.test.tsx9
-rw-r--r--src/components/atoms/icons/close.tsx (renamed from src/components/Icons/Close/Close.tsx)21
-rw-r--r--src/components/atoms/icons/cog.module.scss (renamed from src/components/Icons/Cog/Cog.module.scss)2
-rw-r--r--src/components/atoms/icons/cog.stories.tsx34
-rw-r--r--src/components/atoms/icons/cog.test.tsx9
-rw-r--r--src/components/atoms/icons/cog.tsx (renamed from src/components/Icons/Cog/Cog.tsx)21
-rw-r--r--src/components/atoms/icons/computer-screen.module.scss (renamed from src/components/Icons/Projects/Projects.module.scss)1
-rw-r--r--src/components/atoms/icons/computer-screen.stories.tsx34
-rw-r--r--src/components/atoms/icons/computer-screen.test.tsx9
-rw-r--r--src/components/atoms/icons/computer-screen.tsx (renamed from src/components/Icons/Projects/Projects.tsx)21
-rw-r--r--src/components/atoms/icons/envelop.module.scss (renamed from src/components/Icons/Contact/Contact.module.scss)1
-rw-r--r--src/components/atoms/icons/envelop.stories.tsx34
-rw-r--r--src/components/atoms/icons/envelop.test.tsx9
-rw-r--r--src/components/atoms/icons/envelop.tsx (renamed from src/components/Icons/Contact/Contact.tsx)21
-rw-r--r--src/components/atoms/icons/feed.module.scss (renamed from src/components/Icons/Copyright/Copyright.module.scss)1
-rw-r--r--src/components/atoms/icons/feed.stories.tsx34
-rw-r--r--src/components/atoms/icons/feed.test.tsx9
-rw-r--r--src/components/atoms/icons/feed.tsx74
-rw-r--r--src/components/atoms/icons/hamburger.module.scss (renamed from src/components/Icons/Hamburger/Hamburger.module.scss)40
-rw-r--r--src/components/atoms/icons/hamburger.stories.tsx47
-rw-r--r--src/components/atoms/icons/hamburger.test.tsx9
-rw-r--r--src/components/atoms/icons/hamburger.tsx32
-rw-r--r--src/components/atoms/icons/home.module.scss (renamed from src/components/Icons/Home/Home.module.scss)1
-rw-r--r--src/components/atoms/icons/home.stories.tsx34
-rw-r--r--src/components/atoms/icons/home.test.tsx9
-rw-r--r--src/components/atoms/icons/home.tsx (renamed from src/components/Icons/Home/Home.tsx)21
-rw-r--r--src/components/atoms/icons/magnifying-glass.module.scss (renamed from src/components/Icons/Search/Search.module.scss)2
-rw-r--r--src/components/atoms/icons/magnifying-glass.stories.tsx34
-rw-r--r--src/components/atoms/icons/magnifying-glass.test.tsx9
-rw-r--r--src/components/atoms/icons/magnifying-glass.tsx (renamed from src/components/Icons/Search/Search.tsx)21
-rw-r--r--src/components/atoms/icons/moon.module.scss (renamed from src/components/Icons/Moon/Moon.module.scss)2
-rw-r--r--src/components/atoms/icons/moon.stories.tsx47
-rw-r--r--src/components/atoms/icons/moon.test.tsx9
-rw-r--r--src/components/atoms/icons/moon.tsx (renamed from src/components/Icons/Moon/Moon.tsx)29
-rw-r--r--src/components/atoms/icons/plus-minus.module.scss39
-rw-r--r--src/components/atoms/icons/plus-minus.stories.tsx49
-rw-r--r--src/components/atoms/icons/plus-minus.test.tsx9
-rw-r--r--src/components/atoms/icons/plus-minus.tsx31
-rw-r--r--src/components/atoms/icons/posts-stack.module.scss (renamed from src/components/Icons/Blog/Blog.module.scss)1
-rw-r--r--src/components/atoms/icons/posts-stack.stories.tsx34
-rw-r--r--src/components/atoms/icons/posts-stack.test.tsx9
-rw-r--r--src/components/atoms/icons/posts-stack.tsx (renamed from src/components/Icons/Blog/Blog.tsx)21
-rw-r--r--src/components/atoms/icons/sun.module.scss (renamed from src/components/Icons/Sun/Sun.module.scss)0
-rw-r--r--src/components/atoms/icons/sun.stories.tsx47
-rw-r--r--src/components/atoms/icons/sun.test.tsx9
-rw-r--r--src/components/atoms/icons/sun.tsx (renamed from src/components/Icons/Sun/Sun.tsx)34
-rw-r--r--src/components/atoms/images/logo.module.scss (renamed from src/components/Branding/Logo/Logo.module.scss)7
-rw-r--r--src/components/atoms/images/logo.stories.tsx34
-rw-r--r--src/components/atoms/images/logo.test.tsx9
-rw-r--r--src/components/atoms/images/logo.tsx (renamed from src/components/Branding/Logo/Logo.tsx)18
-rw-r--r--src/components/atoms/layout/column.stories.tsx29
-rw-r--r--src/components/atoms/layout/column.test.tsx12
-rw-r--r--src/components/atoms/layout/column.tsx16
-rw-r--r--src/components/atoms/layout/copyright.module.scss (renamed from src/components/Copyright/Copyright.module.scss)5
-rw-r--r--src/components/atoms/layout/copyright.stories.tsx58
-rw-r--r--src/components/atoms/layout/copyright.test.tsx32
-rw-r--r--src/components/atoms/layout/copyright.tsx59
-rw-r--r--src/components/atoms/layout/main.stories.tsx58
-rw-r--r--src/components/atoms/layout/main.test.tsx12
-rw-r--r--src/components/atoms/layout/main.tsx27
-rw-r--r--src/components/atoms/layout/no-script.module.scss19
-rw-r--r--src/components/atoms/layout/no-script.stories.tsx62
-rw-r--r--src/components/atoms/layout/no-script.test.tsx11
-rw-r--r--src/components/atoms/layout/no-script.tsx21
-rw-r--r--src/components/atoms/layout/notice.module.scss (renamed from src/components/Notice/Notice.module.scss)5
-rw-r--r--src/components/atoms/layout/notice.stories.tsx86
-rw-r--r--src/components/atoms/layout/notice.test.tsx11
-rw-r--r--src/components/atoms/layout/notice.tsx38
-rw-r--r--src/components/atoms/layout/section.module.scss25
-rw-r--r--src/components/atoms/layout/section.stories.tsx102
-rw-r--r--src/components/atoms/layout/section.test.tsx17
-rw-r--r--src/components/atoms/layout/section.tsx57
-rw-r--r--src/components/atoms/layout/sidebar.module.scss12
-rw-r--r--src/components/atoms/layout/sidebar.stories.tsx60
-rw-r--r--src/components/atoms/layout/sidebar.test.tsx11
-rw-r--r--src/components/atoms/layout/sidebar.tsx32
-rw-r--r--src/components/atoms/links/link.module.scss220
-rw-r--r--src/components/atoms/links/link.stories.tsx180
-rw-r--r--src/components/atoms/links/link.test.tsx9
-rw-r--r--src/components/atoms/links/link.tsx67
-rw-r--r--src/components/atoms/links/nav-link.module.scss46
-rw-r--r--src/components/atoms/links/nav-link.stories.tsx55
-rw-r--r--src/components/atoms/links/nav-link.test.tsx12
-rw-r--r--src/components/atoms/links/nav-link.tsx36
-rw-r--r--src/components/atoms/links/sharing-link.module.scss (renamed from src/components/Widgets/Sharing/Sharing.module.scss)44
-rw-r--r--src/components/atoms/links/sharing-link.stories.tsx98
-rw-r--r--src/components/atoms/links/sharing-link.test.tsx46
-rw-r--r--src/components/atoms/links/sharing-link.tsx48
-rw-r--r--src/components/atoms/links/social-link.module.scss (renamed from src/components/Widgets/SocialMedia/SocialMedia.module.scss)30
-rw-r--r--src/components/atoms/links/social-link.stories.tsx73
-rw-r--r--src/components/atoms/links/social-link.test.tsx15
-rw-r--r--src/components/atoms/links/social-link.tsx53
-rw-r--r--src/components/atoms/lists/description-list-item.module.scss40
-rw-r--r--src/components/atoms/lists/description-list-item.stories.tsx132
-rw-r--r--src/components/atoms/lists/description-list-item.test.tsx17
-rw-r--r--src/components/atoms/lists/description-list-item.tsx73
-rw-r--r--src/components/atoms/lists/description-list.module.scss17
-rw-r--r--src/components/atoms/lists/description-list.stories.tsx131
-rw-r--r--src/components/atoms/lists/description-list.test.tsx20
-rw-r--r--src/components/atoms/lists/description-list.tsx103
-rw-r--r--src/components/atoms/lists/list.module.scss45
-rw-r--r--src/components/atoms/lists/list.stories.tsx111
-rw-r--r--src/components/atoms/lists/list.test.tsx26
-rw-r--r--src/components/atoms/lists/list.tsx79
-rw-r--r--src/components/atoms/loaders/progress-bar.module.scss (renamed from src/components/PaginationCursor/PaginationCursor.module.scss)16
-rw-r--r--src/components/atoms/loaders/progress-bar.stories.tsx93
-rw-r--r--src/components/atoms/loaders/progress-bar.test.tsx9
-rw-r--r--src/components/atoms/loaders/progress-bar.tsx55
-rw-r--r--src/components/atoms/loaders/spinner.module.scss (renamed from src/components/Spinner/Spinner.module.scss)0
-rw-r--r--src/components/atoms/loaders/spinner.stories.tsx42
-rw-r--r--src/components/atoms/loaders/spinner.test.tsx14
-rw-r--r--src/components/atoms/loaders/spinner.tsx (renamed from src/components/Spinner/Spinner.tsx)17
-rw-r--r--src/components/molecules/buttons/back-to-top.module.scss51
-rw-r--r--src/components/molecules/buttons/back-to-top.stories.tsx47
-rw-r--r--src/components/molecules/buttons/back-to-top.test.tsx10
-rw-r--r--src/components/molecules/buttons/back-to-top.tsx43
-rw-r--r--src/components/molecules/buttons/heading-button.module.scss44
-rw-r--r--src/components/molecules/buttons/heading-button.stories.tsx105
-rw-r--r--src/components/molecules/buttons/heading-button.test.tsx32
-rw-r--r--src/components/molecules/buttons/heading-button.tsx67
-rw-r--r--src/components/molecules/buttons/help-button.module.scss21
-rw-r--r--src/components/molecules/buttons/help-button.stories.tsx47
-rw-r--r--src/components/molecules/buttons/help-button.test.tsx9
-rw-r--r--src/components/molecules/buttons/help-button.tsx37
-rw-r--r--src/components/molecules/forms/ackee-select.module.scss (renamed from src/components/Settings/AckeeSelect/AckeeSelect.module.scss)7
-rw-r--r--src/components/molecules/forms/ackee-select.stories.tsx71
-rw-r--r--src/components/molecules/forms/ackee-select.test.tsx25
-rw-r--r--src/components/molecules/forms/ackee-select.tsx103
-rw-r--r--src/components/molecules/forms/flipping-label.module.scss63
-rw-r--r--src/components/molecules/forms/flipping-label.stories.tsx96
-rw-r--r--src/components/molecules/forms/flipping-label.test.tsx14
-rw-r--r--src/components/molecules/forms/flipping-label.tsx40
-rw-r--r--src/components/molecules/forms/labelled-field.module.scss9
-rw-r--r--src/components/molecules/forms/labelled-field.stories.tsx293
-rw-r--r--src/components/molecules/forms/labelled-field.test.tsx19
-rw-r--r--src/components/molecules/forms/labelled-field.tsx50
-rw-r--r--src/components/molecules/forms/labelled-select.module.scss9
-rw-r--r--src/components/molecules/forms/labelled-select.stories.tsx236
-rw-r--r--src/components/molecules/forms/labelled-select.test.tsx25
-rw-r--r--src/components/molecules/forms/labelled-select.tsx69
-rw-r--r--src/components/molecules/forms/motion-toggle.stories.tsx57
-rw-r--r--src/components/molecules/forms/motion-toggle.test.tsx13
-rw-r--r--src/components/molecules/forms/motion-toggle.tsx75
-rw-r--r--src/components/molecules/forms/prism-theme-toggle.stories.tsx34
-rw-r--r--src/components/molecules/forms/prism-theme-toggle.test.tsx13
-rw-r--r--src/components/molecules/forms/prism-theme-toggle.tsx73
-rw-r--r--src/components/molecules/forms/select-with-tooltip.module.scss48
-rw-r--r--src/components/molecules/forms/select-with-tooltip.stories.tsx210
-rw-r--r--src/components/molecules/forms/select-with-tooltip.test.tsx32
-rw-r--r--src/components/molecules/forms/select-with-tooltip.tsx73
-rw-r--r--src/components/molecules/forms/theme-toggle.stories.tsx34
-rw-r--r--src/components/molecules/forms/theme-toggle.test.tsx13
-rw-r--r--src/components/molecules/forms/theme-toggle.tsx64
-rw-r--r--src/components/molecules/forms/toggle.module.scss (renamed from src/components/FormElements/Toggle/Toggle.module.scss)8
-rw-r--r--src/components/molecules/forms/toggle.stories.tsx121
-rw-r--r--src/components/molecules/forms/toggle.test.tsx29
-rw-r--r--src/components/molecules/forms/toggle.tsx78
-rw-r--r--src/components/molecules/images/flipping-logo.module.scss59
-rw-r--r--src/components/molecules/images/flipping-logo.stories.tsx72
-rw-r--r--src/components/molecules/images/flipping-logo.test.tsx25
-rw-r--r--src/components/molecules/images/flipping-logo.tsx55
-rw-r--r--src/components/molecules/images/responsive-image.module.scss79
-rw-r--r--src/components/molecules/images/responsive-image.stories.tsx212
-rw-r--r--src/components/molecules/images/responsive-image.test.tsx18
-rw-r--r--src/components/molecules/images/responsive-image.tsx95
-rw-r--r--src/components/molecules/layout/branding.module.scss105
-rw-r--r--src/components/molecules/layout/branding.stories.tsx97
-rw-r--r--src/components/molecules/layout/branding.test.tsx61
-rw-r--r--src/components/molecules/layout/branding.tsx119
-rw-r--r--src/components/molecules/layout/card.module.scss87
-rw-r--r--src/components/molecules/layout/card.stories.tsx176
-rw-r--r--src/components/molecules/layout/card.test.tsx49
-rw-r--r--src/components/molecules/layout/card.tsx98
-rw-r--r--src/components/molecules/layout/code.module.scss305
-rw-r--r--src/components/molecules/layout/code.stories.tsx110
-rw-r--r--src/components/molecules/layout/code.test.tsx16
-rw-r--r--src/components/molecules/layout/code.tsx64
-rw-r--r--src/components/molecules/layout/columns.module.scss30
-rw-r--r--src/components/molecules/layout/columns.stories.tsx108
-rw-r--r--src/components/molecules/layout/columns.test.tsx48
-rw-r--r--src/components/molecules/layout/columns.tsx49
-rw-r--r--src/components/molecules/layout/meta.module.scss5
-rw-r--r--src/components/molecules/layout/meta.stories.tsx69
-rw-r--r--src/components/molecules/layout/meta.test.tsx24
-rw-r--r--src/components/molecules/layout/meta.tsx391
-rw-r--r--src/components/molecules/layout/page-footer.stories.tsx60
-rw-r--r--src/components/molecules/layout/page-footer.test.tsx9
-rw-r--r--src/components/molecules/layout/page-footer.tsx28
-rw-r--r--src/components/molecules/layout/page-header.module.scss (renamed from src/components/PostHeader/PostHeader.module.scss)33
-rw-r--r--src/components/molecules/layout/page-header.stories.tsx113
-rw-r--r--src/components/molecules/layout/page-header.test.tsx18
-rw-r--r--src/components/molecules/layout/page-header.tsx67
-rw-r--r--src/components/molecules/layout/widget.module.scss65
-rw-r--r--src/components/molecules/layout/widget.stories.tsx117
-rw-r--r--src/components/molecules/layout/widget.test.tsx19
-rw-r--r--src/components/molecules/layout/widget.tsx66
-rw-r--r--src/components/molecules/modals/modal.module.scss38
-rw-r--r--src/components/molecules/modals/modal.stories.tsx96
-rw-r--r--src/components/molecules/modals/modal.test.tsx18
-rw-r--r--src/components/molecules/modals/modal.tsx81
-rw-r--r--src/components/molecules/modals/tooltip.module.scss46
-rw-r--r--src/components/molecules/modals/tooltip.stories.tsx70
-rw-r--r--src/components/molecules/modals/tooltip.test.tsx24
-rw-r--r--src/components/molecules/modals/tooltip.tsx60
-rw-r--r--src/components/molecules/nav/breadcrumb.module.scss (renamed from src/components/Breadcrumb/Breadcrumb.module.scss)10
-rw-r--r--src/components/molecules/nav/breadcrumb.stories.tsx81
-rw-r--r--src/components/molecules/nav/breadcrumb.test.tsx15
-rw-r--r--src/components/molecules/nav/breadcrumb.tsx127
-rw-r--r--src/components/molecules/nav/nav.module.scss22
-rw-r--r--src/components/molecules/nav/nav.stories.tsx107
-rw-r--r--src/components/molecules/nav/nav.test.tsx28
-rw-r--r--src/components/molecules/nav/nav.tsx85
-rw-r--r--src/components/molecules/nav/pagination.module.scss51
-rw-r--r--src/components/molecules/nav/pagination.stories.tsx171
-rw-r--r--src/components/molecules/nav/pagination.test.tsx26
-rw-r--r--src/components/molecules/nav/pagination.tsx220
-rw-r--r--src/components/organisms/forms/comment-form.module.scss8
-rw-r--r--src/components/organisms/forms/comment-form.stories.tsx123
-rw-r--r--src/components/organisms/forms/comment-form.test.tsx23
-rw-r--r--src/components/organisms/forms/comment-form.tsx193
-rw-r--r--src/components/organisms/forms/contact-form.module.scss8
-rw-r--r--src/components/organisms/forms/contact-form.stories.tsx65
-rw-r--r--src/components/organisms/forms/contact-form.test.tsx48
-rw-r--r--src/components/organisms/forms/contact-form.tsx158
-rw-r--r--src/components/organisms/forms/search-form.module.scss58
-rw-r--r--src/components/organisms/forms/search-form.stories.tsx65
-rw-r--r--src/components/organisms/forms/search-form.test.tsx16
-rw-r--r--src/components/organisms/forms/search-form.tsx76
-rw-r--r--src/components/organisms/forms/settings-form.module.scss11
-rw-r--r--src/components/organisms/forms/settings-form.stories.tsx67
-rw-r--r--src/components/organisms/forms/settings-form.test.tsx67
-rw-r--r--src/components/organisms/forms/settings-form.tsx56
-rw-r--r--src/components/organisms/images/gallery.module.scss (renamed from src/components/MDX/Gallery/Gallery.module.scss)6
-rw-r--r--src/components/organisms/images/gallery.stories.tsx75
-rw-r--r--src/components/organisms/images/gallery.test.tsx38
-rw-r--r--src/components/organisms/images/gallery.tsx35
-rw-r--r--src/components/organisms/layout/cards-list.module.scss32
-rw-r--r--src/components/organisms/layout/cards-list.stories.tsx136
-rw-r--r--src/components/organisms/layout/cards-list.test.tsx55
-rw-r--r--src/components/organisms/layout/cards-list.tsx77
-rw-r--r--src/components/organisms/layout/comment.fixture.tsx41
-rw-r--r--src/components/organisms/layout/comment.module.scss91
-rw-r--r--src/components/organisms/layout/comment.stories.tsx128
-rw-r--r--src/components/organisms/layout/comment.test.tsx47
-rw-r--r--src/components/organisms/layout/comment.tsx171
-rw-r--r--src/components/organisms/layout/comments-list.fixture.tsx106
-rw-r--r--src/components/organisms/layout/comments-list.module.scss16
-rw-r--r--src/components/organisms/layout/comments-list.stories.tsx91
-rw-r--r--src/components/organisms/layout/comments-list.test.tsx12
-rw-r--r--src/components/organisms/layout/comments-list.tsx60
-rw-r--r--src/components/organisms/layout/footer.module.scss41
-rw-r--r--src/components/organisms/layout/footer.stories.tsx90
-rw-r--r--src/components/organisms/layout/footer.test.tsx33
-rw-r--r--src/components/organisms/layout/footer.tsx77
-rw-r--r--src/components/organisms/layout/header.module.scss50
-rw-r--r--src/components/organisms/layout/header.stories.tsx153
-rw-r--r--src/components/organisms/layout/header.test.tsx46
-rw-r--r--src/components/organisms/layout/header.tsx48
-rw-r--r--src/components/organisms/layout/no-results.stories.tsx28
-rw-r--r--src/components/organisms/layout/no-results.test.tsx14
-rw-r--r--src/components/organisms/layout/no-results.tsx38
-rw-r--r--src/components/organisms/layout/overview.module.scss44
-rw-r--r--src/components/organisms/layout/overview.stories.tsx77
-rw-r--r--src/components/organisms/layout/overview.test.tsx26
-rw-r--r--src/components/organisms/layout/overview.tsx61
-rw-r--r--src/components/organisms/layout/posts-list.fixture.tsx63
-rw-r--r--src/components/organisms/layout/posts-list.module.scss62
-rw-r--r--src/components/organisms/layout/posts-list.stories.tsx194
-rw-r--r--src/components/organisms/layout/posts-list.test.tsx46
-rw-r--r--src/components/organisms/layout/posts-list.tsx239
-rw-r--r--src/components/organisms/layout/summary.fixture.tsx25
-rw-r--r--src/components/organisms/layout/summary.module.scss121
-rw-r--r--src/components/organisms/layout/summary.stories.tsx107
-rw-r--r--src/components/organisms/layout/summary.test.tsx54
-rw-r--r--src/components/organisms/layout/summary.tsx136
-rw-r--r--src/components/organisms/modals/search-modal.module.scss11
-rw-r--r--src/components/organisms/modals/search-modal.stories.tsx47
-rw-r--r--src/components/organisms/modals/search-modal.test.tsx9
-rw-r--r--src/components/organisms/modals/search-modal.tsx37
-rw-r--r--src/components/organisms/modals/settings-modal.module.scss11
-rw-r--r--src/components/organisms/modals/settings-modal.stories.tsx67
-rw-r--r--src/components/organisms/modals/settings-modal.test.tsx14
-rw-r--r--src/components/organisms/modals/settings-modal.tsx51
-rw-r--r--src/components/organisms/toolbar/main-nav.module.scss96
-rw-r--r--src/components/organisms/toolbar/main-nav.stories.tsx91
-rw-r--r--src/components/organisms/toolbar/main-nav.test.tsx33
-rw-r--r--src/components/organisms/toolbar/main-nav.tsx80
-rw-r--r--src/components/organisms/toolbar/search.module.scss3
-rw-r--r--src/components/organisms/toolbar/search.stories.tsx88
-rw-r--r--src/components/organisms/toolbar/search.test.tsx14
-rw-r--r--src/components/organisms/toolbar/search.tsx80
-rw-r--r--src/components/organisms/toolbar/settings.module.scss10
-rw-r--r--src/components/organisms/toolbar/settings.stories.tsx112
-rw-r--r--src/components/organisms/toolbar/settings.test.tsx32
-rw-r--r--src/components/organisms/toolbar/settings.tsx74
-rw-r--r--src/components/organisms/toolbar/toolbar-items.module.scss91
-rw-r--r--src/components/organisms/toolbar/toolbar.module.scss98
-rw-r--r--src/components/organisms/toolbar/toolbar.stories.tsx90
-rw-r--r--src/components/organisms/toolbar/toolbar.test.tsx23
-rw-r--r--src/components/organisms/toolbar/toolbar.tsx77
-rw-r--r--src/components/organisms/widgets/image-widget.module.scss47
-rw-r--r--src/components/organisms/widgets/image-widget.stories.tsx181
-rw-r--r--src/components/organisms/widgets/image-widget.test.tsx59
-rw-r--r--src/components/organisms/widgets/image-widget.tsx69
-rw-r--r--src/components/organisms/widgets/links-list-widget.module.scss75
-rw-r--r--src/components/organisms/widgets/links-list-widget.stories.tsx122
-rw-r--r--src/components/organisms/widgets/links-list-widget.test.tsx32
-rw-r--r--src/components/organisms/widgets/links-list-widget.tsx85
-rw-r--r--src/components/organisms/widgets/sharing.module.scss10
-rw-r--r--src/components/organisms/widgets/sharing.stories.tsx91
-rw-r--r--src/components/organisms/widgets/sharing.test.tsx23
-rw-r--r--src/components/organisms/widgets/sharing.tsx214
-rw-r--r--src/components/organisms/widgets/social-media.module.scss10
-rw-r--r--src/components/organisms/widgets/social-media.stories.tsx61
-rw-r--r--src/components/organisms/widgets/social-media.test.tsx33
-rw-r--r--src/components/organisms/widgets/social-media.tsx41
-rw-r--r--src/components/organisms/widgets/table-of-contents.module.scss4
-rw-r--r--src/components/organisms/widgets/table-of-contents.stories.tsx54
-rw-r--r--src/components/organisms/widgets/table-of-contents.test.tsx12
-rw-r--r--src/components/organisms/widgets/table-of-contents.tsx55
-rw-r--r--src/components/templates/layout/layout.module.scss53
-rw-r--r--src/components/templates/layout/layout.stories.tsx117
-rw-r--r--src/components/templates/layout/layout.test.tsx35
-rw-r--r--src/components/templates/layout/layout.tsx242
-rw-r--r--src/components/templates/page/page-layout.module.scss92
-rw-r--r--src/components/templates/page/page-layout.stories.tsx387
-rw-r--r--src/components/templates/page/page-layout.test.tsx107
-rw-r--r--src/components/templates/page/page-layout.tsx297
-rw-r--r--src/components/templates/sectioned/sectioned-layout.stories.tsx80
-rw-r--r--src/components/templates/sectioned/sectioned-layout.test.tsx41
-rw-r--r--src/components/templates/sectioned/sectioned-layout.tsx60
m---------src/content0
-rw-r--r--src/i18n/en.json908
-rw-r--r--src/i18n/fr.json908
-rw-r--r--src/pages/404.tsx165
-rw-r--r--src/pages/_app.tsx10
-rw-r--r--src/pages/article/[slug].tsx424
-rw-r--r--src/pages/blog/index.tsx366
-rw-r--r--src/pages/blog/page/[id].tsx205
-rw-r--r--src/pages/blog/page/[number].tsx237
-rw-r--r--src/pages/contact.tsx231
-rw-r--r--src/pages/cv.tsx276
-rw-r--r--src/pages/index.tsx327
-rw-r--r--src/pages/mentions-legales.tsx185
-rw-r--r--src/pages/projet/[slug].tsx186
-rw-r--r--src/pages/projets.tsx128
-rw-r--r--src/pages/projets/[slug].tsx241
-rw-r--r--src/pages/projets/index.tsx123
-rw-r--r--src/pages/recherche/index.tsx348
-rw-r--r--src/pages/sujet/[slug].tsx360
-rw-r--r--src/pages/thematique/[slug].tsx331
-rw-r--r--src/services/graphql/api.ts319
-rw-r--r--src/services/graphql/articles.query.ts191
-rw-r--r--src/services/graphql/articles.ts200
-rw-r--r--src/services/graphql/comments.mutation.ts30
-rw-r--r--src/services/graphql/comments.query.ts21
-rw-r--r--src/services/graphql/comments.ts102
-rw-r--r--src/services/graphql/contact.mutation.ts25
-rw-r--r--src/services/graphql/contact.ts26
-rw-r--r--src/services/graphql/mutations.ts82
-rw-r--r--src/services/graphql/queries.ts535
-rw-r--r--src/services/graphql/thematics.query.ts125
-rw-r--r--src/services/graphql/thematics.ts162
-rw-r--r--src/services/graphql/topics.query.ts137
-rw-r--r--src/services/graphql/topics.ts164
-rw-r--r--src/services/local-storage/index.ts6
-rw-r--r--src/styles/abstracts/_placeholders.scss1
-rw-r--r--src/styles/abstracts/placeholders/_layout.scss25
-rw-r--r--src/styles/base/_base.scss16
-rw-r--r--src/styles/base/_fonts.scss12
-rw-r--r--src/styles/base/_helpers.scss6
-rw-r--r--src/styles/base/_spacings.scss10
-rw-r--r--src/styles/base/_typography.scss64
-rw-r--r--src/styles/components/_wp-blocks.scss166
-rw-r--r--src/styles/globals.scss10
-rw-r--r--src/styles/pages/Home.module.scss49
-rw-r--r--src/styles/pages/Projects.module.scss13
-rw-r--r--src/styles/pages/article.module.scss37
-rw-r--r--src/styles/pages/contact.module.scss3
-rw-r--r--src/styles/pages/cv.module.scss3
-rw-r--r--src/styles/pages/home.module.scss36
-rw-r--r--src/styles/pages/partials/_article-headings.scss57
-rw-r--r--src/styles/pages/partials/_article-links.scss204
-rw-r--r--src/styles/pages/partials/_article-lists.scss65
-rw-r--r--src/styles/pages/partials/_article-media.scss20
-rw-r--r--src/styles/pages/partials/_article-prism.scss302
-rw-r--r--src/styles/pages/partials/_article-wp-blocks.scss177
-rw-r--r--src/styles/pages/project.module.scss10
-rw-r--r--src/styles/pages/projects.module.scss11
-rw-r--r--src/styles/pages/topic.module.scss6
-rw-r--r--src/styles/vendors/_prism.scss297
-rw-r--r--src/ts/types/app.ts204
-rw-r--r--src/ts/types/articles.ts102
-rw-r--r--src/ts/types/blog.ts41
-rw-r--r--src/ts/types/comments.ts61
-rw-r--r--src/ts/types/contact.ts19
-rw-r--r--src/ts/types/cover.ts9
-rw-r--r--src/ts/types/mdx.ts17
-rw-r--r--src/ts/types/nav.ts5
-rw-r--r--src/ts/types/prism.ts51
-rw-r--r--src/ts/types/raw-data.ts105
-rw-r--r--src/ts/types/repos.ts7
-rw-r--r--src/ts/types/seo.ts6
-rw-r--r--src/ts/types/swr.ts5
-rw-r--r--src/ts/types/taxonomies.ts114
-rw-r--r--src/utils/config.ts3
-rw-r--r--src/utils/helpers/author.ts32
-rw-r--r--src/utils/helpers/dates.ts40
-rw-r--r--src/utils/helpers/format.ts310
-rw-r--r--src/utils/helpers/i18n.ts2
-rw-r--r--src/utils/helpers/images.ts18
-rw-r--r--src/utils/helpers/pages.ts85
-rw-r--r--src/utils/helpers/prism.ts34
-rw-r--r--src/utils/helpers/projects.ts109
-rw-r--r--src/utils/helpers/rss.ts51
-rw-r--r--src/utils/helpers/schema-org.ts224
-rw-r--r--src/utils/helpers/slugify.ts18
-rw-r--r--src/utils/helpers/sort.ts21
-rw-r--r--src/utils/helpers/strings.ts39
-rw-r--r--src/utils/hooks/use-add-classname.tsx34
-rw-r--r--src/utils/hooks/use-attributes.tsx52
-rw-r--r--src/utils/hooks/use-breadcrumb.tsx107
-rw-r--r--src/utils/hooks/use-click-outside.tsx46
-rw-r--r--src/utils/hooks/use-data-from-api.tsx23
-rw-r--r--src/utils/hooks/use-github-api.tsx (renamed from src/utils/hooks/useGithubApi.tsx)15
-rw-r--r--src/utils/hooks/use-headings-tree.tsx (renamed from src/utils/hooks/useHeadingsTree.tsx)89
-rw-r--r--src/utils/hooks/use-input-autofocus.tsx39
-rw-r--r--src/utils/hooks/use-is-mounted.tsx19
-rw-r--r--src/utils/hooks/use-local-storage.tsx35
-rw-r--r--src/utils/hooks/use-pagination.tsx117
-rw-r--r--src/utils/hooks/use-prism.tsx182
-rw-r--r--src/utils/hooks/use-query-selector-all.tsx24
-rw-r--r--src/utils/hooks/use-reading-time.tsx58
-rw-r--r--src/utils/hooks/use-redirection.tsx33
-rw-r--r--src/utils/hooks/use-route-change.tsx12
-rw-r--r--src/utils/hooks/use-scroll-position.tsx15
-rw-r--r--src/utils/hooks/use-settings.tsx118
-rw-r--r--src/utils/hooks/use-styles.tsx29
-rw-r--r--src/utils/hooks/use-update-ackee-options.tsx19
-rw-r--r--src/utils/providers/ackee.tsx3
-rw-r--r--src/utils/providers/prism-theme.tsx80
-rw-r--r--yarn.lock7469
614 files changed, 36543 insertions, 13051 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index e79fc56..321f511 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,5 +1,9 @@
{
- "extends": ["next/core-web-vitals", "prettier"],
+ "extends": [
+ "next/core-web-vitals",
+ "prettier",
+ "plugin:storybook/recommended"
+ ],
"plugins": ["formatjs"],
"rules": {
"formatjs/enforce-default-message": ["error", "literal"],
diff --git a/.storybook/main.js b/.storybook/main.js
new file mode 100644
index 0000000..477b09e
--- /dev/null
+++ b/.storybook/main.js
@@ -0,0 +1,55 @@
+const path = require('path');
+
+/**
+ * @typedef {import('webpack').Configuration} WebpackConfig
+ */
+
+const storybookConfig = {
+ stories: ['../src/**/*.stories.@(md|mdx|js|jsx|ts|tsx)'],
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ 'storybook-addon-next',
+ 'storybook-dark-mode',
+ ],
+ framework: '@storybook/react',
+ core: {
+ builder: 'webpack5',
+ },
+ staticDirs: ['../public'],
+ /**
+ * @param {WebpackConfig} config
+ * @return {Promise<WebpackConfig>}
+ */
+ webpackFinal: async (config) => {
+ // Use SVGR for SVG files. See: https://medium.com/@derek_19900/config-storybook-4-to-use-svgr-webpack-plugin-22cb1152f004
+ const rules = config.module.rules;
+ const fileLoaderRule = rules.find((rule) => rule.test.test('.svg'));
+ fileLoaderRule.exclude = /\.svg$/;
+ rules.push({
+ test: /\.svg$/,
+ use: [{ loader: '@svgr/webpack', options: { dimensions: false } }],
+ });
+
+ /** @type {import('next').NextConfig} */
+ const nextConfig = require('../next.config');
+
+ // Set modules aliases.
+ config.resolve.alias = {
+ ...config.resolve.alias,
+ '@i18n': path.resolve(__dirname, '../src/i18n'),
+ '@assets': path.resolve(__dirname, '../src/assets'),
+ '@components': path.resolve(__dirname, '../src/components'),
+ '@content': path.resolve(__dirname, '../src/content'),
+ '@pages': path.resolve(__dirname, '../src/pages'),
+ '@services': path.resolve(__dirname, '../src/services'),
+ '@styles': path.resolve(__dirname, '../src/styles'),
+ '@utils': path.resolve(__dirname, '../src/utils'),
+ };
+
+ return { ...config, ...nextConfig.webpack };
+ },
+};
+
+module.exports = storybookConfig;
diff --git a/.storybook/manager.js b/.storybook/manager.js
new file mode 100644
index 0000000..945c246
--- /dev/null
+++ b/.storybook/manager.js
@@ -0,0 +1,6 @@
+import { addons } from '@storybook/addons';
+import light from './themes/light';
+
+addons.setConfig({
+ theme: light,
+});
diff --git a/.storybook/overrides/docs-container.js b/.storybook/overrides/docs-container.js
new file mode 100644
index 0000000..f539986
--- /dev/null
+++ b/.storybook/overrides/docs-container.js
@@ -0,0 +1,36 @@
+import { DocsContainer as BaseContainer } from '@storybook/addon-docs/blocks';
+import { useDarkMode } from 'storybook-dark-mode';
+import dark from '../themes/dark';
+import light from '../themes/light';
+
+/**
+ * Custom Docs Container to support dark theme.
+ *
+ * @see https://github.com/hipstersmoothie/storybook-dark-mode/issues/127#issuecomment-1070524402
+ */
+export const DocsContainer = ({ children, context }) => {
+ const isDark = useDarkMode();
+
+ return (
+ <BaseContainer
+ context={{
+ ...context,
+ storyById: (id) => {
+ const storyContext = context.storyById(id);
+ return {
+ ...storyContext,
+ parameters: {
+ ...storyContext?.parameters,
+ docs: {
+ ...storyContext?.parameters?.docs,
+ theme: isDark ? dark : light,
+ },
+ },
+ };
+ },
+ }}
+ >
+ {children}
+ </BaseContainer>
+ );
+};
diff --git a/.storybook/preview.js b/.storybook/preview.js
new file mode 100644
index 0000000..9df7514
--- /dev/null
+++ b/.storybook/preview.js
@@ -0,0 +1,74 @@
+import * as NextImage from 'next/image';
+import { ThemeProvider, useTheme } from 'next-themes';
+import { useEffect } from 'react';
+import { IntlProvider } from 'react-intl';
+import { useDarkMode } from 'storybook-dark-mode';
+import { DocsContainer } from './overrides/docs-container';
+import dark from './themes/dark';
+import light from './themes/light';
+import '@styles/globals.scss';
+
+const OriginalNextImage = NextImage.default;
+
+Object.defineProperty(NextImage, 'default', {
+ configurable: true,
+ value: (props) =>
+ typeof props.src === 'string' ? (
+ <OriginalNextImage {...props} unoptimized blurDataURL={props.src} />
+ ) : (
+ <OriginalNextImage {...props} unoptimized />
+ ),
+});
+
+Object.defineProperty(NextImage, '__esModule', {
+ configurable: true,
+ value: true,
+});
+
+export const parameters = {
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+ darkMode: {
+ // Override the default dark theme
+ dark: { ...dark },
+ // Override the default light theme
+ light: { ...light },
+ stylePreview: true,
+ },
+ docs: {
+ container: DocsContainer,
+ },
+};
+
+// Create a component that listens for theme change.
+export const ThemeWrapper = (props) => {
+ const { setTheme } = useTheme();
+ const theme = useDarkMode() ? 'dark' : 'light';
+
+ useEffect(() => {
+ setTheme(theme);
+ }, [theme, setTheme]);
+
+ return <>{props.children}</>;
+};
+
+export const decorators = [
+ (Story) => (
+ <IntlProvider locale="en">
+ <ThemeProvider
+ defaultTheme="system"
+ enableColorScheme={true}
+ enableSystem={true}
+ >
+ <ThemeWrapper>
+ <Story />
+ </ThemeWrapper>
+ </ThemeProvider>
+ </IntlProvider>
+ ),
+];
diff --git a/.storybook/themes/common.js b/.storybook/themes/common.js
new file mode 100644
index 0000000..17619c2
--- /dev/null
+++ b/.storybook/themes/common.js
@@ -0,0 +1,8 @@
+export const brand = {
+ title: 'Design system',
+};
+
+export const fontFamilies = {
+ mono: '"Cousine", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace',
+ primary: '"Inter", "Liberation Sans", Arial, sans-serif',
+};
diff --git a/.storybook/themes/dark.js b/.storybook/themes/dark.js
new file mode 100644
index 0000000..e23703b
--- /dev/null
+++ b/.storybook/themes/dark.js
@@ -0,0 +1,37 @@
+import { create } from '@storybook/theming';
+import { brand, fontFamilies } from './common';
+
+const colors = {
+ black: 'hsl(208, 25%, 11%)',
+ blackBright: 'hsl(208, 21%, 15%)',
+ blue: 'hsl(200, 50%, 68%)',
+ blueBright: 'hsl(200, 55%, 70%)',
+ grey: 'hsl(208, 10%, 70%)',
+ greyDark: 'hsl(208, 20%, 25%)',
+ greyDarker: 'hsl(208, 18%, 20%)',
+ white: 'hsl(208, 25%, 92%)',
+ whiteDark: 'hsl(206, 20%, 93%)',
+};
+
+export default create({
+ base: 'dark',
+ brandTitle: brand.title,
+ colorPrimary: colors.blue,
+ colorSecondary: colors.blueBright,
+ appBg: colors.black,
+ appContentBg: colors.black,
+ appBorderColor: colors.greyDark,
+ appBorderRadius: 3,
+ fontBase: fontFamilies.primary,
+ fontCode: fontFamilies.mono,
+ textColor: colors.white,
+ textInverseColor: colors.black,
+ textMutedColor: colors.grey,
+ barTextColor: colors.white,
+ barSelectedColor: colors.blueBright,
+ barBg: colors.blackBright,
+ inputBg: colors.greyDarker,
+ inputBorder: colors.greyDark,
+ inputTextColor: colors.white,
+ inputBorderRadius: 0,
+});
diff --git a/.storybook/themes/light.js b/.storybook/themes/light.js
new file mode 100644
index 0000000..d445272
--- /dev/null
+++ b/.storybook/themes/light.js
@@ -0,0 +1,36 @@
+import { create } from '@storybook/theming';
+import { brand, fontFamilies } from './common';
+
+const colors = {
+ black: 'hsl(207, 47%, 11%)',
+ blue: 'hsl(206, 75%, 31%)',
+ blueBright: 'hsl(206, 77%, 36%)',
+ grey: 'hsl(206, 15%, 80%)',
+ greyBright: 'hsl(206, 20%, 86%)',
+ greyDark: 'hsl(206, 30%, 30%)',
+ white: 'hsl(206, 15%, 97%)',
+ whiteDark: 'hsl(206, 20%, 93%)',
+};
+
+export default create({
+ base: 'light',
+ brandTitle: brand.title,
+ colorPrimary: colors.blue,
+ colorSecondary: colors.blueBright,
+ appBg: colors.white,
+ appContentBg: colors.white,
+ appBorderColor: colors.grey,
+ appBorderRadius: 3,
+ fontBase: fontFamilies.primary,
+ fontCode: fontFamilies.mono,
+ textColor: colors.black,
+ textInverseColor: colors.white,
+ textMutedColor: colors.greyDark,
+ barTextColor: colors.black,
+ barSelectedColor: colors.blueBright,
+ barBg: colors.whiteDark,
+ inputBg: colors.greyBright,
+ inputBorder: colors.grey,
+ inputTextColor: colors.black,
+ inputBorderRadius: 0,
+});
diff --git a/README.md b/README.md
index cd4277b..e11189b 100644
--- a/README.md
+++ b/README.md
@@ -79,6 +79,23 @@ yarn dev
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Then, you can make your changes.
+### Storybook
+
+You can search for a component or develop a new component in isolation thanks to Storybook.
+
+```bash
+yarn storybook
+```
+
+The different components are divided into 4 categories (atomic design):
+
+- Atoms
+- Molecules
+- Organisms
+- Templates
+
+But, to be honest, between _Molecules_ and _Organisms_ some components may be misclassified.
+
### i18n
When editing strings that require translation, run:
diff --git a/__tests__/jest/components/Branding.test.tsx b/__tests__/jest/components/Branding.test.tsx
deleted file mode 100644
index ae759a3..0000000
--- a/__tests__/jest/components/Branding.test.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import Branding from '@components/Branding/Branding';
-import { render, screen } from '@test-utils';
-import '../__mocks__/matchMedia.mock';
-
-describe('Branding', () => {
- it('renders the title wrapped with an h1 element on homepage', () => {
- render(<Branding isHome={true} />);
- expect(
- screen.getByRole('heading', { level: 1, name: 'Armand Philippot' })
- ).toBeInTheDocument();
- });
-
- it('renders the title wrapped without an h1 element on other pages', () => {
- render(<Branding isHome={false} />);
- expect(
- screen.queryByRole('heading', { level: 1, name: 'Armand Philippot' })
- ).not.toBeInTheDocument();
- });
-
- it('renders the baseline', () => {
- render(<Branding isHome={false} />);
- // Currently, only French translation is returned.
- expect(screen.getByText('Intégrateur web')).toBeInTheDocument();
- });
-});
diff --git a/__tests__/jest/components/Copyright.test.tsx b/__tests__/jest/components/Copyright.test.tsx
deleted file mode 100644
index 5f6f287..0000000
--- a/__tests__/jest/components/Copyright.test.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import Copyright from '@components/Copyright/Copyright';
-import { render, screen } from '@test-utils';
-import '../__mocks__/matchMedia.mock';
-
-describe('Copyright', () => {
- it('renders the Copyright component', () => {
- render(<Copyright />);
- });
-
- it('displays author name', () => {
- render(<Copyright />);
- expect(screen.getByText('Armand Philippot')).toBeInTheDocument();
- });
-});
diff --git a/__tests__/jest/components/Header.test.tsx b/__tests__/jest/components/Header.test.tsx
deleted file mode 100644
index de85e95..0000000
--- a/__tests__/jest/components/Header.test.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import Header from '@components/Header/Header';
-import { render } from '@test-utils';
-import '../__mocks__/matchMedia.mock';
-
-// Toolbar uses forwardRef. Without mocking an error occurred.
-jest.mock('@components/Toolbar/Toolbar', () => 'div');
-
-describe('Header', () => {
- it('renders the Header component', () => {
- const { container } = render(<Header isHome={false} />);
- expect(container).toBeTruthy();
- });
-});
diff --git a/__tests__/utils/test-utils.tsx b/__tests__/utils/test-utils.tsx
index 00123c3..1bcea8e 100644
--- a/__tests__/utils/test-utils.tsx
+++ b/__tests__/utils/test-utils.tsx
@@ -1,9 +1,10 @@
import { render, RenderOptions } from '@testing-library/react';
import { ThemeProvider } from 'next-themes';
-import { FC, ReactElement } from 'react';
+import { FC, ReactElement, ReactNode } from 'react';
import { IntlProvider } from 'react-intl';
type ProvidersConfig = {
+ children: ReactNode;
locale?: 'en' | 'fr';
};
diff --git a/jest.setup.js b/jest.setup.js
index 1088ac5..d50c988 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -1,3 +1,5 @@
import '@testing-library/jest-dom/extend-expect';
+import './__tests__/jest/__mocks__/matchMedia.mock';
jest.mock('next/dist/client/router', () => require('next-router-mock'));
+jest.mock('next/dynamic', () => () => 'dynamic-import');
diff --git a/mdx.d.ts b/mdx.d.ts
index f3a9a90..b4e333d 100644
--- a/mdx.d.ts
+++ b/mdx.d.ts
@@ -1,13 +1,9 @@
declare module '*.mdx' {
+ import { MDXData, MDXPageMeta, MDXProjectMeta } from '@ts/types/mdx';
import { MDXProps } from 'mdx/types';
- import { Meta } from '@ts/types/app';
let MDXComponent: (props: MDXProps) => JSX.Element;
export default MDXComponent;
- export const cover: string;
- export const image: string;
- export const intro: string;
- export const meta: Meta;
- export const pdf: string;
- export const seo: { title: string; description: string };
+ export const data: MDXData;
+ export const meta: MDXPageMeta | MDXProjectMeta;
}
diff --git a/next.config.js b/next.config.js
index fc35b57..872efe2 100644
--- a/next.config.js
+++ b/next.config.js
@@ -16,7 +16,7 @@ const contentSecurityPolicy = `
font-src 'self';
frame-src 'self';
img-src 'self' ${backendDomain} secure.gravatar.com data:;
- media-src 'self' data:;
+ media-src 'self' ${backendDomain} data:;
script-src 'self' ${ackeeDomain} 'unsafe-inline' data:;
style-src 'self' 'unsafe-inline';
`;
@@ -28,7 +28,7 @@ const contentSecurityPolicyDev = `
font-src 'self';
frame-src 'self';
img-src 'self' ${backendDomain} secure.gravatar.com data:;
- media-src 'self' data:;
+ media-src 'self' ${backendDomain} data:;
object-src 'self' data:;
script-src 'self' ${ackeeDomain} 'unsafe-inline' 'unsafe-eval' data:;
style-src 'self' 'unsafe-inline';
diff --git a/package.json b/package.json
index bd85a9c..447a372 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,9 @@
"test": "jest",
"test:ci": "jest --ci",
"test:coverage": "jest --coverage",
- "test:watch": "jest --watch"
+ "test:watch": "jest --watch",
+ "storybook": "start-storybook -p 6006",
+ "build-storybook": "build-storybook"
},
"dependencies": {
"@formatjs/swc-plugin": "^1.4.0",
@@ -42,7 +44,6 @@
"@next/mdx": "^12.1.5",
"feed": "^4.2.2",
"graphql": "^16.1.0",
- "graphql-request": "^4.2.0",
"modern-normalize": "^1.1.0",
"next": "^12.1.5",
"next-sitemap": "^2.5.20",
@@ -50,16 +51,26 @@
"prismjs": "^1.27.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
- "react-intl": "^5.24.8",
+ "react-intl": "^5.25.0",
"schema-dts": "^1.1.0",
"sharp": "^0.30.3",
"swr": "^1.3.0",
"use-ackee": "^3.0.1"
},
"devDependencies": {
+ "@babel/core": "^7.17.8",
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@formatjs/cli": "^4.8.3",
+ "@storybook/addon-essentials": "^6.5.3",
+ "@storybook/addon-interactions": "^6.5.3",
+ "@storybook/addon-links": "^6.5.3",
+ "@storybook/addons": "^6.5.3",
+ "@storybook/builder-webpack5": "^6.5.3",
+ "@storybook/manager-webpack5": "^6.5.3",
+ "@storybook/react": "^6.5.3",
+ "@storybook/testing-library": "^0.0.11",
+ "@storybook/theming": "^6.5.3",
"@svgr/webpack": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
@@ -73,17 +84,22 @@
"eslint-config-next": "^12.1.5",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-formatjs": "^3.1.0",
+ "eslint-plugin-storybook": "^0.5.12",
"eslint-plugin-testing-library": "^5.3.1",
"husky": "^7.0.4",
"jest": "^27.5.1",
"lint-staged": "^12.3.8",
"next-router-mock": "^0.6.7",
+ "postcss": "^8.4.12",
"prettier": "^2.6.2",
"sass": "^1.50.0",
"standard-version": "^9.3.2",
+ "storybook-addon-next": "^1.6.2",
+ "storybook-dark-mode": "^1.1.0",
"stylelint": "^14.7.0",
"stylelint-config-standard": "^25.0.0",
"stylelint-config-standard-scss": "^3.0.0",
- "typescript": "^4.6.3"
+ "typescript": "^4.6.3",
+ "webpack": "^5.70.0"
}
}
diff --git a/src/components/Branding/Branding.module.scss b/src/components/Branding/Branding.module.scss
deleted file mode 100644
index 2cd3b15..0000000
--- a/src/components/Branding/Branding.module.scss
+++ /dev/null
@@ -1,169 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.wrapper {
- --logo-size: clamp(#{fun.convert-px(68)}, 18vw, #{fun.convert-px(100)});
-
- display: grid;
- grid-template-columns:
- var(--logo-size)
- minmax(0, 1fr);
- grid-template-rows: repeat(2, max-content);
- align-items: center;
- column-gap: var(--spacing-sm);
- padding: var(--spacing-sm) 0;
- text-shadow: fun.convert-px(2) fun.convert-px(2) 0 var(--color-fg-inverted);
-}
-
-.logo {
- --branding-logo-animation: none;
-
- grid-column: 1;
- grid-row: 1 / -1;
- justify-self: center;
- display: flex;
- place-content: center;
- width: var(--logo-size);
- height: var(--logo-size);
- position: relative;
- border-radius: 50%;
- transition: all 0.6s linear 0s;
- transform-style: preserve-3d;
- animation: var(--branding-logo-animation);
-
- &__front,
- &__back {
- width: 100%;
- height: 100%;
- padding: fun.convert-px(2);
- position: absolute;
- top: 0;
- left: 0;
- backface-visibility: hidden;
- background: var(--color-bg);
- border: fun.convert-px(2) solid var(--color-primary-dark);
- border-radius: 50%;
- transition: all 0.6s linear 0s;
- }
-
- &__front {
- box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0
- var(--color-shadow-light),
- fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0
- var(--color-shadow-light);
- }
-
- &__back {
- transform: rotateY(180deg);
- }
-
- img,
- svg {
- border-radius: 50%;
- }
-
- &:hover {
- transform: rotateY(180deg);
- }
-
- &:hover & {
- &__front {
- box-shadow: none;
- }
-
- &__back {
- box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0
- var(--color-shadow-light),
- fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0
- var(--color-shadow-light);
- }
- }
-}
-
-.name {
- --branding-name-animation: none;
-
- grid-column: 2;
- grid-row: 1;
- margin: 0;
- font-family: var(--font-family-secondary);
- font-size: clamp(var(--font-size-xl), 6vw, var(--font-size-2xl));
- font-weight: 500;
- letter-spacing: 0.01ex;
- position: relative;
- overflow: hidden;
-
- &::after {
- content: "|";
- display: block;
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- right: 0;
- background: var(--color-bg);
- color: var(--color-primary-darker);
- font-weight: 400;
- visibility: hidden;
- transform: translateX(100%);
- transform-origin: right;
- animation: var(--branding-name-animation);
- }
-}
-
-.job {
- --branding-job-animation: none;
-
- grid-column: 2;
- grid-row: 2;
- width: max-content;
- margin: 0;
- color: var(--color-fg-light);
- font-family: var(--font-family-secondary);
- font-size: var(--font-size-lg);
- font-weight: 500;
- position: relative;
- overflow: hidden;
-
- &::after {
- content: "|";
- display: block;
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- right: 0;
- background: var(--color-bg);
- color: var(--color-primary-darker);
- font-weight: 400;
- visibility: hidden;
- transform: translateX(100%);
- transform-origin: right;
- animation: var(--branding-job-animation);
- }
-}
-
-.link {
- background: linear-gradient(
- to top,
- var(--color-primary-light) fun.convert-px(5),
- transparent fun.convert-px(5)
- )
- left / 0 100% no-repeat;
- text-decoration: none;
- transition: all 0.6s ease-out 0s;
-
- &:hover,
- &:focus {
- background-size: 100% 100%;
- }
-
- &:focus {
- color: var(--color-primary-light);
- }
-
- &:active {
- background-size: 0 100%;
- color: var(--color-primary-dark);
- }
-}
diff --git a/src/components/Branding/Branding.tsx b/src/components/Branding/Branding.tsx
deleted file mode 100644
index b19116d..0000000
--- a/src/components/Branding/Branding.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import photo from '@assets/images/armand-philippot.jpg';
-import { settings } from '@utils/config';
-import Image from 'next/image';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { ReactElement, useEffect, useRef } from 'react';
-import { useIntl } from 'react-intl';
-import { Person, WithContext } from 'schema-dts';
-import styles from './Branding.module.scss';
-import Logo from './Logo/Logo';
-
-type BrandingReturn = ({ isHome }: { isHome: boolean }) => ReactElement;
-
-const Branding: BrandingReturn = ({ isHome = false }) => {
- const intl = useIntl();
- const { locale } = useRouter();
- const TitleTag = isHome ? 'h1' : 'p';
- const logoRef = useRef<HTMLDivElement>(null);
- const titleRef = useRef<HTMLHeadingElement | HTMLParagraphElement>(null);
- const jobRef = useRef<HTMLParagraphElement>(null);
-
- useEffect(() => {
- if (logoRef.current) {
- logoRef.current.style.setProperty(
- '--branding-logo-animation',
- 'flip-logo 9s ease-in 0s 1'
- );
- }
- }, []);
-
- useEffect(() => {
- if (titleRef.current) {
- titleRef.current.style.setProperty(
- '--branding-name-animation',
- 'blink 0.8s ease-in-out 0s 2, typing 4.3s linear 0s 1'
- );
- }
- }, []);
-
- useEffect(() => {
- if (jobRef.current) {
- jobRef.current.style.setProperty(
- '--branding-job-animation',
- 'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1'
- );
- }
- }, []);
-
- const schemaJsonLd: WithContext<Person> = {
- '@context': 'https://schema.org',
- '@type': 'Person',
- '@id': `${settings.url}/#branding`,
- name: settings.name,
- url: settings.url,
- jobTitle: locale?.startsWith('en')
- ? settings.baseline.en
- : settings.baseline.fr,
- image: photo.src,
- subjectOf: { '@id': `${settings.url}` },
- };
-
- return (
- <>
- <Script
- id="schema-branding"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <div id="branding" className={styles.wrapper}>
- <div className={styles.logo} ref={logoRef}>
- <div className={styles.logo__front}>
- <Image
- src={photo}
- alt={intl.formatMessage(
- {
- defaultMessage: '{brandingName} picture',
- description: 'Branding: branding name picture.',
- id: 'ILRLTq',
- },
- {
- brandingName: settings.name,
- }
- )}
- layout="responsive"
- />
- </div>
- <div className={styles.logo__back}>
- <Logo />
- </div>
- </div>
- <TitleTag ref={titleRef} className={styles.name}>
- <Link href="/">
- <a className={styles.link}>{settings.name}</a>
- </Link>
- </TitleTag>
- <p ref={jobRef} className={styles.job}>
- {locale?.startsWith('en')
- ? settings.baseline.en
- : settings.baseline.fr}
- </p>
- </div>
- </>
- );
-};
-
-export default Branding;
diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx
deleted file mode 100644
index a7b945a..0000000
--- a/src/components/Breadcrumb/Breadcrumb.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { settings } from '@utils/config';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { useIntl } from 'react-intl';
-import { BreadcrumbList, WithContext } from 'schema-dts';
-import styles from './Breadcrumb.module.scss';
-
-const Breadcrumb = ({ pageTitle }: { pageTitle: string }) => {
- const intl = useIntl();
- const router = useRouter();
-
- const isHome = router.pathname === '/';
- const isArticle = router.pathname.includes('/article/');
- const isProject = router.pathname.includes('/projet/');
- const isSubject = router.pathname.includes('/sujet/');
- const isThematic = router.pathname.includes('/thematique/');
-
- const getItems = () => {
- return (
- <>
- <li className={styles.item}>
- <Link href="/">
- <a>
- {intl.formatMessage({
- defaultMessage: 'Home',
- description: 'Breadcrumb: Home item',
- id: 'Enij19',
- })}
- </a>
- </Link>
- </li>
- {(isArticle || isThematic || isSubject) && (
- <>
- <li className={styles.item}>
- <Link href="/blog">
- <a>
- {intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'Breadcrumb: Blog item',
- id: 'z0ic9c',
- })}
- </a>
- </Link>
- </li>
- </>
- )}
- {isProject && (
- <>
- <li className={styles.item}>
- <Link href="/projets">
- <a>
- {intl.formatMessage({
- defaultMessage: 'Projects',
- description: 'Breadcrumb: Projects item',
- id: 'Igx3qp',
- })}
- </a>
- </Link>
- </li>
- </>
- )}
- <li className="screen-reader-text">{pageTitle}</li>
- </>
- );
- };
-
- const getElementsSchema = () => {
- const items = [];
- const homepage: BreadcrumbList['itemListElement'] = {
- '@type': 'ListItem',
- position: 1,
- name: intl.formatMessage({
- defaultMessage: 'Home',
- description: 'Breadcrumb: Home item',
- id: 'Enij19',
- }),
- item: settings.url,
- };
-
- items.push(homepage);
-
- if (isArticle || isThematic || isSubject) {
- const blog: BreadcrumbList['itemListElement'] = {
- '@type': 'ListItem',
- position: 2,
- name: intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'Breadcrumb: Blog item',
- id: 'z0ic9c',
- }),
- item: `${settings.url}/blog`,
- };
-
- items.push(blog);
- }
-
- if (isProject) {
- const blog: BreadcrumbList['itemListElement'] = {
- '@type': 'ListItem',
- position: 2,
- name: intl.formatMessage({
- defaultMessage: 'Projects',
- description: 'Breadcrumb: Projects item',
- id: 'Igx3qp',
- }),
- item: `${settings.url}/projets`,
- };
-
- items.push(blog);
- }
-
- const currentPage: BreadcrumbList['itemListElement'] = {
- '@type': 'ListItem',
- position: items.length + 1,
- name: pageTitle,
- item: `${settings.url}${router.asPath}`,
- };
-
- items.push(currentPage);
-
- return items;
- };
-
- const schemaJsonLd: WithContext<BreadcrumbList> = {
- '@context': 'https://schema.org',
- '@type': 'BreadcrumbList',
- '@id': `${settings.url}/#breadcrumb`,
- itemListElement: getElementsSchema(),
- };
-
- return (
- <>
- <Script
- id="schema-breadcrumb"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- {!isHome && (
- <nav id="breadcrumb" className={styles.wrapper}>
- <span className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'You are here:',
- description: 'Breadcrumb: You are here prefix',
- id: '16zl9Z',
- })}
- </span>
- <ol className={styles.list}>{getItems()}</ol>
- </nav>
- )}
- </>
- );
-};
-
-export default Breadcrumb;
diff --git a/src/components/Buttons/Button/Button.tsx b/src/components/Buttons/Button/Button.tsx
deleted file mode 100644
index ada23fe..0000000
--- a/src/components/Buttons/Button/Button.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ButtonKind, ButtonPosition } from '@ts/types/app';
-import { ReactNode } from 'react';
-import styles from '../Buttons.module.scss';
-
-const Button = ({
- children,
- clickHandler,
- kind = 'secondary',
- position = 'left',
- spacing = false,
- isDisabled = false,
-}: {
- children: ReactNode;
- clickHandler: any;
- kind?: ButtonKind;
- position?: ButtonPosition;
- spacing?: boolean;
- isDisabled?: boolean;
-}) => {
- const spacingClass = spacing ? styles.spacing : '';
- const classes = `${styles.btn} ${styles[position]} ${styles[kind]} ${spacingClass}`;
-
- return (
- <button
- className={classes}
- type="button"
- disabled={isDisabled}
- onClick={clickHandler}
- >
- {children}
- </button>
- );
-};
-
-export default Button;
diff --git a/src/components/Buttons/ButtonHelp/ButtonHelp.module.scss b/src/components/Buttons/ButtonHelp/ButtonHelp.module.scss
deleted file mode 100644
index b2a05d7..0000000
--- a/src/components/Buttons/ButtonHelp/ButtonHelp.module.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.icon {
- color: var(--color-primary-dark);
- font-weight: 600;
-}
-
-.active {
- .icon {
- color: var(--color-fg-inverted);
- }
-}
-
-.wrapper {
- width: fun.convert-px(44);
- height: fun.convert-px(44);
- background: var(--color-bg);
- border: fun.convert-px(3) solid var(--color-primary-dark);
- border-radius: 50%;
- box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow);
- transition: all 0.3s ease-in-out 0s;
-
- @include mix.pointer("fine") {
- width: fun.convert-px(30);
- height: fun.convert-px(30);
- line-height: 1;
- }
-
- &:hover,
- &:focus {
- border-color: var(--color-primary-light);
- color: var(--color-primary-light);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow-light),
- fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) fun.convert-px(-3)
- var(--color-shadow-light);
- transform: scale(1.1);
-
- .icon {
- transform: scale(1.1);
- }
- }
-
- &.active {
- background: var(--color-primary);
- }
-}
diff --git a/src/components/Buttons/ButtonHelp/ButtonHelp.tsx b/src/components/Buttons/ButtonHelp/ButtonHelp.tsx
deleted file mode 100644
index 2616a8f..0000000
--- a/src/components/Buttons/ButtonHelp/ButtonHelp.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { SetStateAction } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './ButtonHelp.module.scss';
-
-const ButtonHelp = ({
- showHelp,
- setShowHelp,
- title,
-}: {
- showHelp: boolean;
- setShowHelp: (value: SetStateAction<boolean>) => void;
- title?: string;
-}) => {
- const intl = useIntl();
-
- const handleClick = () => {
- setShowHelp((prev) => !prev);
- };
-
- const activeModifier = showHelp ? styles.active : '';
-
- return (
- <button
- onClick={handleClick}
- title={title}
- className={`${styles.wrapper} ${activeModifier}`}
- >
- <span className={styles.icon} aria-hidden="true">
- ?
- </span>
- <span className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'Help',
- description: 'ButtonHelp: screen reader text',
- id: 'oPf+XA',
- })}
- </span>
- </button>
- );
-};
-
-export default ButtonHelp;
diff --git a/src/components/Buttons/ButtonLink/ButtonLink.tsx b/src/components/Buttons/ButtonLink/ButtonLink.tsx
deleted file mode 100644
index 179c686..0000000
--- a/src/components/Buttons/ButtonLink/ButtonLink.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { ButtonPosition } from '@ts/types/app';
-import Link from 'next/link';
-import { ReactNode } from 'react';
-import styles from '../Buttons.module.scss';
-
-const ButtonLink = ({
- children,
- target,
- position = 'left',
- isExternal = false,
-}: {
- children: ReactNode;
- target: string;
- position?: ButtonPosition;
- isExternal?: boolean;
-}) => {
- const classes = `${styles.btn} ${styles[position]} ${styles.tertiary}`;
-
- return isExternal ? (
- <a className={classes} href={target}>
- {children}
- </a>
- ) : (
- <Link href={target}>
- <a className={classes}>{children}</a>
- </Link>
- );
-};
-
-export default ButtonLink;
diff --git a/src/components/Buttons/ButtonSubmit/ButtonSubmit.tsx b/src/components/Buttons/ButtonSubmit/ButtonSubmit.tsx
deleted file mode 100644
index 4725cad..0000000
--- a/src/components/Buttons/ButtonSubmit/ButtonSubmit.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ReactNode } from 'react';
-import styles from '../Buttons.module.scss';
-
-type Modifier = 'search' | 'submit';
-
-const ButtonSubmit = ({
- children,
- modifier = 'submit',
-}: {
- children: ReactNode;
- modifier?: Modifier;
-}) => {
- const withModifier = modifier === 'search' ? styles.search : styles.primary;
-
- return (
- <button type="submit" className={`${styles.btn} ${withModifier}`}>
- {children}
- </button>
- );
-};
-
-export default ButtonSubmit;
diff --git a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx b/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx
deleted file mode 100644
index 7ceb70d..0000000
--- a/src/components/Buttons/ButtonToolbar/ButtonToolbar.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { CloseIcon, CogIcon, SearchIcon } from '@components/Icons';
-import { ForwardedRef, forwardRef, SetStateAction } from 'react';
-import { useIntl } from 'react-intl';
-import styles from '../Buttons.module.scss';
-
-type ButtonType = 'search' | 'settings';
-
-const ButtonToolbar = (
- {
- type,
- isActivated,
- setIsActivated,
- }: {
- type: ButtonType;
- isActivated: boolean;
- setIsActivated: (value: SetStateAction<boolean>) => void;
- },
- ref: ForwardedRef<HTMLButtonElement>
-) => {
- const intl = useIntl();
- const ButtonIcon = () => (type === 'search' ? <SearchIcon /> : <CogIcon />);
- const btnClasses = isActivated
- ? `${styles.toolbar} ${styles['toolbar--activated']}`
- : styles.toolbar;
-
- return (
- <button
- ref={ref}
- className={btnClasses}
- type="button"
- onClick={() => setIsActivated(!isActivated)}
- >
- <span className={styles.icon}>
- <span className={styles.front}>
- <ButtonIcon />
- </span>
- <span className={styles.back}>
- <CloseIcon />
- </span>
- </span>
- {isActivated ? (
- <span className="screen-reader-text">
- {intl.formatMessage(
- {
- defaultMessage: 'Close {type}',
- description: 'ButtonToolbar: Close button',
- id: 'SWq8a4',
- },
- {
- type,
- }
- )}
- </span>
- ) : (
- <span className="screen-reader-text">
- {intl.formatMessage(
- {
- defaultMessage: 'Open {type}',
- description: 'ButtonToolbar: Open button',
- id: 'Z1eSIz',
- },
- {
- type,
- }
- )}
- </span>
- )}
- </button>
- );
-};
-
-export default forwardRef(ButtonToolbar);
diff --git a/src/components/Buttons/Buttons.module.scss b/src/components/Buttons/Buttons.module.scss
deleted file mode 100644
index 0ea9289..0000000
--- a/src/components/Buttons/Buttons.module.scss
+++ /dev/null
@@ -1,289 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-@use "@styles/abstracts/placeholders";
-
-.btn {
- display: block;
- border: none;
- font-size: var(--font-size-md);
-}
-
-.left {
- margin-right: auto;
-}
-
-.right {
- margin-left: auto;
-}
-
-.center {
- margin-left: auto;
- margin-right: auto;
-}
-
-.primary {
- margin: auto;
- padding: var(--spacing-2xs) var(--spacing-md);
- background: var(--color-primary);
- border: fun.convert-px(2) solid var(--color-bg);
- border-radius: fun.convert-px(5);
- box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary),
- 0 0 0 fun.convert-px(3) var(--color-primary-darker),
- fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(3)
- var(--color-primary-dark);
- color: var(--color-fg-inverted);
- font-weight: 600;
- text-shadow: fun.convert-px(2) fun.convert-px(2) 0 var(--color-shadow);
- transition: all 0.25s ease-in-out 0s;
-
- &:hover,
- &:focus {
- background: var(--color-primary-light);
- box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary-light),
- 0 0 0 fun.convert-px(3) var(--color-primary-darker),
- fun.convert-px(7) fun.convert-px(7) 0 fun.convert-px(2)
- var(--color-primary-dark);
- transform: translateX(#{fun.convert-px(-4)})
- translateY(#{fun.convert-px(-4)});
- }
-
- &:focus {
- text-decoration: underline solid var(--color-fg-inverted) fun.convert-px(2);
- }
-
- &:active {
- background: var(--color-primary-dark);
- box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary),
- 0 0 0 fun.convert-px(3) var(--color-primary-darker),
- 0 0 0 0 var(--color-primary-dark);
- text-decoration: none;
- transform: translateX(#{fun.convert-px(4)}) translateY(#{fun.convert-px(4)});
- }
-}
-
-.secondary {
- padding: var(--spacing-2xs) var(--spacing-md);
- background: var(--color-bg);
- border: fun.convert-px(3) solid var(--color-primary);
- border-radius: fun.convert-px(5);
- box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg),
- fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-dark),
- fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg),
- fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-dark);
- color: var(--color-primary);
- font-weight: 600;
- transition: all 0.35s ease-out 0s;
-
- &:disabled {
- color: var(--color-fg-light);
- border-color: var(--color-border-dark);
- box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg),
- fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-darker),
- fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg),
- fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-darker);
- cursor: wait;
- }
-
- &:not(:disabled) {
- &:hover,
- &:focus {
- transform: translateX(#{fun.convert-px(-3)})
- translateY(#{fun.convert-px(-5)});
- border-color: var(--color-primary-light);
- box-shadow: fun.convert-px(2) fun.convert-px(3) 0 0 var(--color-bg),
- fun.convert-px(4) fun.convert-px(5) 0 0 var(--color-primary),
- fun.convert-px(6) fun.convert-px(8) 0 0 var(--color-bg),
- fun.convert-px(8) fun.convert-px(10) 0 0 var(--color-primary),
- fun.convert-px(10) fun.convert-px(12) fun.convert-px(1) 0
- var(--color-shadow-light),
- fun.convert-px(10) fun.convert-px(12) fun.convert-px(5)
- fun.convert-px(1) var(--color-shadow-light);
- color: var(--color-primary-light);
- }
-
- &:focus {
- text-decoration: underline var(--color-primary) fun.convert-px(2);
- }
-
- &:active {
- text-decoration: none;
- transform: translateX(#{fun.convert-px(5)})
- translateY(#{fun.convert-px(6)});
- box-shadow: 0 0 0 0 var(--color-shadow);
- }
- }
-}
-
-.tertiary {
- display: flex;
- flex-flow: row wrap;
- place-items: center;
- gap: var(--spacing-2xs);
- width: max-content;
- padding: var(--spacing-2xs) var(--spacing-sm);
- position: relative;
- background: var(--color-bg);
- border: fun.convert-px(3) solid var(--color-primary);
- border-radius: fun.convert-px(5);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow);
- color: var(--color-primary);
- font-weight: 600;
- text-decoration: underline transparent 0;
- transition: all 0.3s ease-in-out 0s, text-decoration 0.35s ease-in-out 0s;
-
- &:hover,
- &:focus {
- border-color: var(--color-primary-light);
- color: var(--color-primary-light);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow-light),
- fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) fun.convert-px(-3)
- var(--color-shadow-light);
- transform: scale(1.1);
- }
-
- &:focus {
- text-decoration: underline var(--color-primary-light) fun.convert-px(3);
- }
-
- &:active {
- border-color: var(--color-primary-dark);
- color: var(--color-primary-dark);
- box-shadow: 0 0 0 0 var(--color-shadow);
- text-decoration: underline transparent 0;
- transform: scale(0.94);
- }
-}
-
-:global {
- [data-theme="dark"] {
- :local {
- .tertiary {
- img[src*="png"] {
- background: none;
- }
- }
- }
- }
-}
-
-.toolbar {
- --draw-border-thickness: #{fun.convert-px(4)};
- --icon-size: 100%;
-
- display: flex;
- flex-flow: row nowrap;
- place-items: center;
- width: var(--btn-size, 100%);
- height: var(--btn-size, 100%);
- padding: var(--spacing-2xs);
- background: none;
- border: none;
- font-size: var(--font-size-md);
-
- &:hover,
- &:focus {
- --draw-border-color1: var(--color-primary-light);
- --draw-border-color2: var(--color-primary-lighter);
-
- @extend %draw-borders;
- }
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- border-radius: 8%;
- }
- }
-}
-
-.icon {
- display: block;
- width: 100%;
- height: 100%;
- position: relative;
- transform-style: preserve-3d;
- transform: translate3d(0, 0, 0);
- transition: all 0.5s ease-in-out 0s;
-}
-
-.front,
-.back {
- display: flex;
- place-content: center;
- width: var(--icon-size);
- height: var(--icon-size);
- position: absolute;
- top: 0;
- right: 0;
- transition: all 0.6s ease-in 0s;
- backface-visibility: hidden;
-}
-
-.front {
- transform: scale(1);
- z-index: 20;
-}
-
-.back {
- transform: scale(0.2) rotateY(180deg);
- z-index: 10;
-}
-
-.toolbar--activated {
- .icon {
- transform: rotateY(180deg);
- }
-
- .front {
- transform: scale(0.2);
- }
-
- .back {
- transform: scale(1) rotateY(180deg);
- }
-}
-
-.search {
- background: transparent;
- margin-left: calc(var(--btn-size) * -1);
- z-index: 5;
- transition: all 0.3s ease-in-out 0s;
-
- svg {
- transform: scale(0.85);
- transition: all 0.3s ease-in-out 0s;
- }
-
- &:hover,
- &:focus {
- svg {
- transform: scale(0.85) rotate(20deg) translateY(#{fun.convert-px(3)});
- }
- }
-
- &:focus {
- outline: var(--color-primary-light) solid fun.convert-px(3);
- }
-
- &:active {
- outline: none;
-
- svg {
- transform: scale(0.7);
- }
- }
-}
-
-.spacing {
- margin-top: var(--spacing-md);
- margin-bottom: var(--spacing-md);
-}
diff --git a/src/components/Buttons/index.tsx b/src/components/Buttons/index.tsx
deleted file mode 100644
index 9b4b756..0000000
--- a/src/components/Buttons/index.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import Button from './Button/Button';
-import ButtonLink from './ButtonLink/ButtonLink';
-import ButtonToolbar from './ButtonToolbar/ButtonToolbar';
-import ButtonSubmit from './ButtonSubmit/ButtonSubmit';
-import ButtonHelp from './ButtonHelp/ButtonHelp';
-
-export { Button, ButtonHelp, ButtonLink, ButtonToolbar, ButtonSubmit };
diff --git a/src/components/Comment/Comment.module.scss b/src/components/Comment/Comment.module.scss
deleted file mode 100644
index dd52db2..0000000
--- a/src/components/Comment/Comment.module.scss
+++ /dev/null
@@ -1,99 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.item {
- margin: var(--spacing-sm) 0;
-}
-
-.wrapper {
- background: var(--color-bg);
- border: fun.convert-px(1) solid var(--color-border);
- box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow-light),
- fun.convert-px(4) fun.convert-px(4) fun.convert-px(3) fun.convert-px(-2)
- var(--color-shadow);
- padding: var(--spacing-md);
- position: relative;
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- display: grid;
- grid-template-columns: minmax(#{fun.convert-px(150)}, 1fr) minmax(0, 85ch);
- column-gap: var(--spacing-lg);
- }
- }
-}
-
-.header {
- display: flex;
- flex-flow: column wrap;
- align-items: center;
- row-gap: var(--spacing-sm);
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- grid-row: 1 / 4;
- }
- }
-}
-
-.avatar {
- width: fun.convert-px(85);
- height: fun.convert-px(85);
- margin: 0 auto;
- border-radius: fun.convert-px(3);
- box-shadow: 0 0 0 fun.convert-px(1) var(--color-shadow-light),
- fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(1) var(--color-shadow);
- position: relative;
-
- img {
- border-radius: fun.convert-px(3);
- }
-}
-
-.author {
- color: var(--color-primary-darker);
- font-weight: 600;
- text-align: center;
-}
-
-.date {
- color: var(--color-fg-secondary);
- font-size: var(--font-size-sm);
- display: flex;
- flex-flow: row wrap;
- column-gap: var(--spacing-2xs);
- justify-content: center;
- margin: var(--spacing-md) 0;
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- justify-content: left;
- margin: 0 0 var(--spacing-md);
- }
- }
-}
-
-.body {
- overflow-wrap: break-word;
-}
-
-.footer {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- padding: var(--spacing-md) 0 0;
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- padding: var(--spacing-sm) 0 0;
- }
- }
-
- button {
- margin: 0;
- }
-}
-
-.list {
- padding-left: var(--spacing-md);
-}
diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx
deleted file mode 100644
index 355363b..0000000
--- a/src/components/Comment/Comment.tsx
+++ /dev/null
@@ -1,200 +0,0 @@
-import { Button } from '@components/Buttons';
-import Spinner from '@components/Spinner/Spinner';
-import { Comment as CommentData } from '@ts/types/comments';
-import { settings } from '@utils/config';
-import { getFormattedDate } from '@utils/helpers/format';
-import dynamic from 'next/dynamic';
-import Image from 'next/image';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { useState } from 'react';
-import { useIntl } from 'react-intl';
-import { Comment as CommentSchema, WithContext } from 'schema-dts';
-import styles from './Comment.module.scss';
-
-const DynamicCommentForm = dynamic(
- () => import('@components/CommentForm/CommentForm'),
- {
- loading: () => <Spinner />,
- }
-);
-
-const Comment = ({
- articleId,
- comment,
- isNested = false,
-}: {
- articleId: number;
- comment: CommentData;
- isNested?: boolean;
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const locale = router.locale ? router.locale : settings.locales.defaultLocale;
- const [shouldOpenForm, setShouldOpenForm] = useState<boolean>(false);
-
- const getCommentAuthor = () => {
- return comment.author.url ? (
- <Link href={comment.author.url}>
- <a className={styles.author}>{comment.author.name}</a>
- </Link>
- ) : (
- <span className={styles.author}>{comment.author.name}</span>
- );
- };
-
- const getLocaleDate = () => {
- const date = getFormattedDate(comment.date, locale);
- const time = new Date(comment.date)
- .toLocaleTimeString(locale, {
- hour: 'numeric',
- minute: 'numeric',
- })
- .replace(':', 'h');
- return intl.formatMessage(
- {
- defaultMessage: '{date} at {time}',
- description: 'Comment: publication date',
- id: 'CT3ydM',
- },
- {
- date,
- time,
- }
- );
- };
-
- const getApprovedComment = () => {
- return (
- <>
- <article
- className={styles.wrapper}
- id={`comment-${comment.databaseId}`}
- >
- <header className={styles.header}>
- {comment.author.gravatarUrl && (
- <div className={styles.avatar}>
- <Image
- src={comment.author.gravatarUrl}
- alt={comment.author.name}
- layout="fill"
- />
- </div>
- )}
- {getCommentAuthor()}
- </header>
- <dl className={styles.date}>
- <dt>
- {intl.formatMessage({
- defaultMessage: 'Published on:',
- description: 'Comment: publication date label',
- id: 'soj7do',
- })}
- </dt>
- <dd>
- <time dateTime={comment.date}>
- <Link href={`#comment-${comment.databaseId}`}>
- <a>{getLocaleDate()}</a>
- </Link>
- </time>
- </dd>
- </dl>
- <div
- className={styles.body}
- dangerouslySetInnerHTML={{ __html: comment.content }}
- ></div>
- {!isNested && (
- <footer className={styles.footer}>
- <Button clickHandler={() => setShouldOpenForm((prev) => !prev)}>
- {shouldOpenForm
- ? intl.formatMessage({
- defaultMessage: 'Cancel reply',
- description: 'Comment: reply button',
- id: 'e1Forh',
- })
- : intl.formatMessage({
- defaultMessage: 'Reply',
- description: 'Comment: reply button',
- id: 'hzHuCc',
- })}
- </Button>
- </footer>
- )}
- </article>
- {shouldOpenForm && (
- <DynamicCommentForm
- articleId={articleId}
- parentId={comment.databaseId}
- />
- )}
- {comment.replies.length > 0 && (
- <ol className={styles.list}>
- {comment.replies.map((reply) => {
- return (
- <Comment
- articleId={articleId}
- key={reply.databaseId}
- comment={reply}
- isNested={true}
- />
- );
- })}
- </ol>
- )}
- </>
- );
- };
-
- const getCommentStatus = () => {
- return (
- <p>
- {intl.formatMessage({
- defaultMessage: 'This comment is awaiting moderation.',
- description: 'Comment: awaiting moderation message',
- id: 'rXeTkM',
- })}
- </p>
- );
- };
-
- const schemaJsonLd: WithContext<CommentSchema> = {
- '@context': 'https://schema.org',
- '@id': `${settings.url}/#comment-${comment.databaseId}`,
- '@type': 'Comment',
- parentItem: isNested
- ? { '@id': `${settings.url}/#comment-${comment.parentDatabaseId}` }
- : undefined,
- about: { '@type': 'Article', '@id': `${settings.url}/#article` },
- author: {
- '@type': 'Person',
- name: comment.author.name,
- image: comment.author.gravatarUrl,
- url: comment.author.url,
- },
- creator: {
- '@type': 'Person',
- name: comment.author.name,
- image: comment.author.gravatarUrl,
- url: comment.author.url,
- },
- dateCreated: comment.date,
- datePublished: comment.date,
- text: comment.content,
- };
-
- return (
- <>
- <Script
- id="schema-comments"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <li className={styles.item}>
- {comment.approved ? getApprovedComment() : getCommentStatus()}
- </li>
- </>
- );
-};
-
-export default Comment;
diff --git a/src/components/CommentForm/CommentForm.module.scss b/src/components/CommentForm/CommentForm.module.scss
deleted file mode 100644
index d19a1ef..0000000
--- a/src/components/CommentForm/CommentForm.module.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.wrapper {
- width: min(calc(100vw - (var(--spacing-md) * 2)), fun.convert-px(500));
- margin: auto;
-
- &--reply {
- width: 100%;
- margin-top: var(--spacing-sm);
- padding: var(--spacing-md);
- position: relative;
- background: var(--color-bg);
- border: fun.convert-px(1) solid var(--color-border);
- box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0
- var(--color-shadow-light),
- fun.convert-px(4) fun.convert-px(4) fun.convert-px(3) fun.convert-px(-2)
- var(--color-shadow);
- }
-}
-
-.title {
- width: max-content;
- margin-left: auto;
- margin-right: auto;
-}
diff --git a/src/components/CommentForm/CommentForm.tsx b/src/components/CommentForm/CommentForm.tsx
deleted file mode 100644
index 128dc58..0000000
--- a/src/components/CommentForm/CommentForm.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-import { ButtonSubmit } from '@components/Buttons';
-import { Field, Form, FormItem, Label } from '@components/FormElements';
-import Notice from '@components/Notice/Notice';
-import Spinner from '@components/Spinner/Spinner';
-import { createComment } from '@services/graphql/mutations';
-import { NoticeType } from '@ts/types/app';
-import { useEffect, useRef, useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './CommentForm.module.scss';
-
-const CommentForm = ({
- articleId,
- parentId = 0,
-}: {
- articleId: number;
- parentId?: number;
-}) => {
- const intl = useIntl();
- const [name, setName] = useState<string>('');
- const [email, setEmail] = useState<string>('');
- const [website, setWebsite] = useState<string>('');
- const [comment, setComment] = useState<string>('');
- const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
- const [notice, setNotice] = useState<string>();
- const [noticeType, setNoticeType] = useState<NoticeType>('success');
- const nameFieldRef = useRef<HTMLInputElement>(null);
-
- useEffect(() => {
- if (parentId === 0) return;
- nameFieldRef.current && nameFieldRef.current.focus();
- });
-
- const resetForm = () => {
- setName('');
- setEmail('');
- setWebsite('');
- setComment('');
- setIsSubmitting(false);
- };
-
- const isEmptyString = (value: string): boolean => value.trim() === '';
- const areRequiredFieldsSet = (): boolean =>
- !isEmptyString(name) && !isEmptyString(email) && !isEmptyString(comment);
-
- const sendComment = async () => {
- const data = {
- author: name,
- authorEmail: email,
- authorUrl: website,
- content: comment,
- parent: parentId,
- commentOn: articleId,
- mutationId: 'createComment',
- };
-
- const createdComment = await createComment(data);
-
- if (createdComment.success) {
- resetForm();
- setNoticeType('success');
- if (createdComment.comment?.approved) {
- setNotice(
- intl.formatMessage({
- defaultMessage: 'Thanks for your comment!',
- description: 'CommentForm: success notice',
- id: 'AVUUgG',
- })
- );
- } else {
- setNotice(
- intl.formatMessage({
- defaultMessage:
- 'Thanks for your comment! It is now awaiting moderation.',
- description: 'CommentForm: success notice but awaiting moderation',
- id: 'Ul2NIl',
- })
- );
- }
-
- setTimeout(() => {
- setNotice(undefined);
- }, 10000);
- } else {
- setNoticeType('error');
- setNotice(
- intl.formatMessage({
- defaultMessage:
- 'An unexpected error happened. Comment cannot be submitted.',
- description: 'CommentForm: error notice',
- id: 'cjK9Ad',
- })
- );
- }
- };
-
- const submitHandler = async (e: SubmitEvent) => {
- e.preventDefault();
- setNotice(undefined);
- setIsSubmitting(true);
-
- if (areRequiredFieldsSet()) {
- sendComment();
- } else {
- setIsSubmitting(false);
- setNoticeType('warning');
- setNotice(
- intl.formatMessage({
- defaultMessage:
- 'Some required fields are empty. Comment cannot be submitted.',
- description: 'CommentForm: missing required fields',
- id: 'Rle+UK',
- })
- );
- }
- };
-
- const isReply = parentId !== 0;
- const wrapperClasses = `${styles.wrapper} ${
- isReply ? styles['wrapper--reply'] : ''
- }`;
-
- const getLabel = (
- body: string,
- htmlFor: string,
- required: boolean = false
- ) => {
- return <Label body={body} htmlFor={htmlFor} required={required} />;
- };
-
- const nameLabelBody = intl.formatMessage({
- defaultMessage: 'Name',
- description: 'CommentForm: Name field label',
- id: 'F7QxJH',
- });
-
- const emailLabelBody = intl.formatMessage({
- defaultMessage: 'Email',
- description: 'CommentForm: Email field label',
- id: 'Oim3rQ',
- });
-
- const websiteLabelBody = intl.formatMessage({
- defaultMessage: 'Website',
- description: 'CommentForm: Website field label',
- id: 'jN+dY5',
- });
-
- const commentLabelBody = intl.formatMessage({
- defaultMessage: 'Comment',
- description: 'CommentForm: Comment field label',
- id: 'J4nhm4',
- });
-
- return (
- <div className={wrapperClasses}>
- <h2 className={styles.title}>
- {intl.formatMessage({
- defaultMessage: 'Leave a comment',
- description: 'CommentForm: Form title',
- id: '+aHn7j',
- })}
- </h2>
- <Form
- submitHandler={submitHandler}
- kind={isReply ? 'centered' : undefined}
- >
- <FormItem>
- <Field
- id="commenter-name"
- name="commenter-name"
- label={getLabel(nameLabelBody, 'commenter-name', true)}
- value={name}
- setValue={setName}
- required={true}
- ref={nameFieldRef}
- />
- </FormItem>
- <FormItem>
- <Field
- id="commenter-email"
- name="commenter-email"
- kind="email"
- label={getLabel(emailLabelBody, 'commenter-email', true)}
- value={email}
- setValue={setEmail}
- required={true}
- />
- </FormItem>
- <FormItem>
- <Field
- id="commenter-website"
- name="commenter-website"
- label={getLabel(websiteLabelBody, 'commenter-website')}
- value={website}
- setValue={setWebsite}
- />
- </FormItem>
- <FormItem>
- <Field
- id="commenter-comment"
- name="commenter-comment"
- kind="textarea"
- label={getLabel(commentLabelBody, 'commenter-comment', true)}
- value={comment}
- setValue={setComment}
- required={true}
- />
- </FormItem>
- <FormItem>
- <noscript>
- {intl.formatMessage({
- defaultMessage: 'Javascript is required to post a comment.',
- description: 'CommentForm: noscript tag',
- id: 'g1cFCa',
- })}
- </noscript>
- <ButtonSubmit>
- {intl.formatMessage({
- defaultMessage: 'Send',
- description: 'CommentForm: Send button',
- id: 'WGFOmA',
- })}
- </ButtonSubmit>
- </FormItem>
- </Form>
- {isSubmitting && (
- <Spinner
- message={intl.formatMessage({
- defaultMessage: 'Submitting...',
- description: 'CommentForm: submitting message',
- id: 'HEJ3Gv',
- })}
- />
- )}
- {notice && <Notice type={noticeType}>{notice}</Notice>}
- </div>
- );
-};
-
-export default CommentForm;
diff --git a/src/components/CommentsList/CommentsList.module.scss b/src/components/CommentsList/CommentsList.module.scss
deleted file mode 100644
index 4971b15..0000000
--- a/src/components/CommentsList/CommentsList.module.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@use "@styles/abstracts/placeholders";
-
-.title,
-.no-comments {
- width: max-content;
- margin-left: auto;
- margin-right: auto;
-}
-
-.list {
- @extend %reset-ordered-list;
-
- margin-bottom: var(--spacing-lg);
-}
diff --git a/src/components/CommentsList/CommentsList.tsx b/src/components/CommentsList/CommentsList.tsx
deleted file mode 100644
index 0eaac17..0000000
--- a/src/components/CommentsList/CommentsList.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import Comment from '@components/Comment/Comment';
-import Spinner from '@components/Spinner/Spinner';
-import { getCommentsByPostId } from '@services/graphql/queries';
-import { Comment as CommentData } from '@ts/types/comments';
-import { useIntl } from 'react-intl';
-import useSWR from 'swr';
-import styles from './CommentsList.module.scss';
-
-const CommentsList = ({
- articleId,
- comments,
-}: {
- articleId: number;
- comments: CommentData[];
-}) => {
- const intl = useIntl();
- const { data, error } = useSWR<CommentData[]>(
- '/api/comments',
- () => getCommentsByPostId(articleId),
- { fallbackData: comments }
- );
-
- const getCommentsList = () => {
- if (error) {
- return intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'CommentsList: failed to load',
- id: 'Zlkww3',
- });
- }
-
- if (!data) return <Spinner />;
-
- return data.map((comment) => {
- return (
- <Comment
- key={comment.databaseId}
- articleId={articleId}
- comment={comment}
- />
- );
- });
- };
-
- return (
- <>
- <h2 className={styles.title}>
- {intl.formatMessage({
- defaultMessage: 'Comments',
- description: 'CommentsList: Comments section title',
- id: 'Ns8CFb',
- })}
- </h2>
- {data && data.length > 0 ? (
- <ol className={styles.list}>{getCommentsList()}</ol>
- ) : (
- <p className={styles['no-comments']}>
- {intl.formatMessage({
- defaultMessage: 'No comments yet.',
- description: 'CommentsList: No comment message',
- id: 'e9L59q',
- })}
- </p>
- )}
- </>
- );
-};
-
-export default CommentsList;
diff --git a/src/components/ContactForm/ContactForm.module.scss b/src/components/ContactForm/ContactForm.module.scss
deleted file mode 100644
index 3f0e861..0000000
--- a/src/components/ContactForm/ContactForm.module.scss
+++ /dev/null
@@ -1,21 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.status {
- max-width: max-content;
- margin: var(--spacing-md) 0;
- padding: var(--spacing-sm);
- border: fun.convert-px(3) solid var(--color-border-light);
- border-radius: fun.convert-px(5);
-
- &--error {
- border-color: var(--color-token-red);
- }
-
- &--success {
- border-color: var(--color-token-green);
- }
-
- &--warning {
- border-color: var(--color-token-orange);
- }
-}
diff --git a/src/components/ContactForm/ContactForm.tsx b/src/components/ContactForm/ContactForm.tsx
deleted file mode 100644
index 5af6982..0000000
--- a/src/components/ContactForm/ContactForm.tsx
+++ /dev/null
@@ -1,180 +0,0 @@
-import { ButtonSubmit } from '@components/Buttons';
-import { Field, Form, FormItem, Label } from '@components/FormElements';
-import { sendMail } from '@services/graphql/mutations';
-import { settings } from '@utils/config';
-import { FormEvent, useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './ContactForm.module.scss';
-
-type Status = 'success' | 'error' | 'warning';
-
-const ContactForm = () => {
- const intl = useIntl();
- const [name, setName] = useState('');
- const [email, setEmail] = useState('');
- const [subject, setSubject] = useState('');
- const [message, setMessage] = useState('');
- const [status, setStatus] = useState<Status>();
- const [statusMessage, setStatusMessage] = useState<string>('');
-
- const resetForm = () => {
- setName('');
- setEmail('');
- setSubject('');
- setMessage('');
- };
-
- const submitHandler = async (e: FormEvent) => {
- e.preventDefault();
-
- if (!name || !email || !message) {
- setStatus('warning');
- setStatusMessage(
- intl.formatMessage({
- defaultMessage:
- 'Warning: mail not sent. Some required fields are empty.',
- description: 'ContactForm: missing fields message.',
- id: 'WpycgB',
- })
- );
- return;
- }
-
- const messageHTML = message.replace(/\r?\n/g, '<br />');
- const body = `Message received from ${name} <${email}> on ${settings.url}.<br /><br />${messageHTML}`;
- const replyTo = `${name} <${email}>`;
- const data = {
- body,
- mutationId: 'contact',
- replyTo,
- subject,
- };
- const mail = await sendMail(data);
-
- if (mail.sent) {
- setStatus('success');
- setStatusMessage(
- intl.formatMessage({
- defaultMessage:
- 'Thanks. Your message was successfully sent. I will answer it as soon as possible.',
- description: 'ContactForm: success message',
- id: 'gQKeF+',
- })
- );
- resetForm();
- } else {
- const errorPrefix = intl.formatMessage({
- defaultMessage: 'An error occurred:',
- description: 'ContactForm: error message',
- id: 'pTxT7N',
- });
- const error = `${errorPrefix} ${mail.message}`;
- setStatus('error');
- setStatusMessage(error);
- }
- };
-
- const getStatus = () => {
- if (!status) return <></>;
-
- const statusModifier = `status--${status}`;
-
- return (
- <p className={`${styles.status} ${styles[statusModifier]}`}>
- {statusMessage}
- </p>
- );
- };
-
- const getLabel = (
- body: string,
- htmlFor: string,
- required: boolean = false
- ) => {
- return <Label body={body} htmlFor={htmlFor} required={required} />;
- };
-
- const nameLabelBody = intl.formatMessage({
- defaultMessage: 'Name',
- description: 'ContactForm: name field label',
- id: '6ibqFS',
- });
-
- const emailLabelBody = intl.formatMessage({
- defaultMessage: 'Email',
- description: 'ContactForm: email field label',
- id: 'Vuryko',
- });
-
- const subjectLabelBody = intl.formatMessage({
- defaultMessage: 'Subject',
- description: 'ContactForm: subject field label',
- id: 'uMURuJ',
- });
-
- const messageLabelBody = intl.formatMessage({
- defaultMessage: 'Message',
- description: 'ContactForm: message field label',
- id: '0zBQpa',
- });
-
- return (
- <>
- <Form submitHandler={submitHandler}>
- <FormItem>
- <Field
- id="contact-name"
- name="name"
- value={name}
- setValue={setName}
- required={true}
- label={getLabel(nameLabelBody, 'contact-name', true)}
- />
- </FormItem>
- <FormItem>
- <Field
- id="contact-email"
- kind="email"
- name="email"
- value={email}
- setValue={setEmail}
- required={true}
- label={getLabel(emailLabelBody, 'contact-email', true)}
- />
- </FormItem>
- <FormItem>
- <Field
- id="contact-subject"
- name="subject"
- value={subject}
- setValue={setSubject}
- label={getLabel(subjectLabelBody, 'contact-subject')}
- />
- </FormItem>
- <FormItem>
- <Field
- id="contact-message"
- kind="textarea"
- name="message"
- value={message}
- setValue={setMessage}
- required={true}
- label={getLabel(messageLabelBody, 'contact-message', true)}
- />
- </FormItem>
- <FormItem>
- <ButtonSubmit>
- {intl.formatMessage({
- defaultMessage: 'Send',
- description: 'ContactForm: send button text',
- id: 'X7n7N2',
- })}
- </ButtonSubmit>
- </FormItem>
- </Form>
- {getStatus()}
- </>
- );
-};
-
-export default ContactForm;
diff --git a/src/components/Copyright/Copyright.tsx b/src/components/Copyright/Copyright.tsx
deleted file mode 100644
index d2de2e9..0000000
--- a/src/components/Copyright/Copyright.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { CopyrightIcon } from '@components/Icons';
-import { settings } from '@utils/config';
-import styles from './Copyright.module.scss';
-
-const Copyright = () => {
- return (
- <p className={styles.wrapper}>
- <span className={styles.name}>{settings.name}</span>
- <CopyrightIcon />
- <span>
- {settings.copyright.startYear} - {settings.copyright.endYear}
- </span>
- </p>
- );
-};
-
-export default Copyright;
diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss
deleted file mode 100644
index 1d156f8..0000000
--- a/src/components/Footer/Footer.module.scss
+++ /dev/null
@@ -1,90 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.wrapper {
- display: flex;
- flex-flow: column wrap;
- gap: var(--spacing-xs);
- place-items: center;
- place-content: center;
- padding: var(--spacing-md) 0 calc(var(--toolbar-size) + var(--spacing-md));
- border-top: fun.convert-px(3) solid var(--color-border-light);
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- flex-flow: row wrap;
- font-size: var(--font-size-sm);
- }
- }
-}
-
-.back-to-top {
- --button-size: #{fun.convert-px(55)};
- --icon-size: #{fun.convert-px(32)};
-
- position: fixed;
- bottom: calc(var(--toolbar-size) + var(--spacing-md));
- right: var(--spacing-md);
- transition: all 0.4s ease-in 0s;
-
- &--hidden {
- opacity: 0;
- transform: translateY(calc(var(--button-size) + var(--spacing-md)));
- }
-
- &--visible {
- opacity: 1;
- transform: translateY(0);
- }
-
- a {
- display: flex;
- place-content: center;
- padding: 0;
- width: var(--button-size);
- height: var(--button-size);
-
- svg {
- height: 85%;
- }
-
- :global {
- .arrow-head {
- transform: translateY(30%);
- transition: all 0.45s ease-in-out 0s;
- }
-
- .arrow-bar {
- opacity: 0;
- transform: translateY(30%) translateX(25%) scale(0.5);
- transition: transform 0.45s ease-in-out 0s, opacity 0.3s ease-in-out 0s;
- }
- }
-
- &:hover,
- &:focus {
- :global {
- .arrow-head {
- transform: translateY(0);
- }
-
- .arrow-bar {
- opacity: 1;
- transform: translateY(0) translateX(0) scale(1);
- }
- }
-
- svg {
- :global {
- animation: pulse 1.2s ease-in-out 0.6s infinite;
- }
- }
- }
-
- &:active {
- svg {
- animation-play-state: paused;
- }
- }
- }
-}
diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx
deleted file mode 100644
index 381b4a8..0000000
--- a/src/components/Footer/Footer.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { ButtonLink } from '@components/Buttons';
-import Copyright from '@components/Copyright/Copyright';
-import FooterNav from '@components/FooterNav/FooterNav';
-import { ArrowIcon } from '@components/Icons';
-import { useEffect, useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './Footer.module.scss';
-
-const Footer = () => {
- const intl = useIntl();
- const [backToTopClasses, setBackToTopClasses] = useState(
- `${styles['back-to-top']} ${styles['back-to-top--hidden']}`
- );
-
- const handleScroll = () => {
- const currentScrollY = window.scrollY;
-
- if (currentScrollY > 300) {
- setBackToTopClasses(
- `${styles['back-to-top']} ${styles['back-to-top--visible']}`
- );
- } else {
- setBackToTopClasses(
- `${styles['back-to-top']} ${styles['back-to-top--hidden']}`
- );
- }
- };
-
- useEffect(() => {
- window.addEventListener('scroll', handleScroll);
- return () => window.removeEventListener('scroll', handleScroll);
- }, []);
-
- return (
- <footer className={styles.wrapper}>
- <Copyright />
- <FooterNav />
- <div className={backToTopClasses}>
- <ButtonLink target="#top" position="center">
- <span className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'Back to top',
- description: 'Footer: Back to top button',
- id: 'dqrd6I',
- })}
- </span>
- <ArrowIcon direction="top" />
- </ButtonLink>
- </div>
- </footer>
- );
-};
-
-export default Footer;
diff --git a/src/components/FooterNav/FooterNav.module.scss b/src/components/FooterNav/FooterNav.module.scss
deleted file mode 100644
index 73ea568..0000000
--- a/src/components/FooterNav/FooterNav.module.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-@use "@styles/abstracts/mixins" as mix;
-@use "@styles/abstracts/placeholders";
-
-.list {
- @extend %flex-list;
-
- gap: var(--spacing-xs);
- place-content: center;
-}
-
-.item {
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- &::before {
- content: "\2022";
- margin-right: var(--spacing-xs);
- }
- }
- }
-}
diff --git a/src/components/FooterNav/FooterNav.tsx b/src/components/FooterNav/FooterNav.tsx
deleted file mode 100644
index 763e951..0000000
--- a/src/components/FooterNav/FooterNav.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import Link from 'next/link';
-import styles from './FooterNav.module.scss';
-import { NavItem } from '@ts/types/nav';
-import { useIntl } from 'react-intl';
-
-const FooterNav = () => {
- const intl = useIntl();
-
- const footerNavConfig: NavItem[] = [
- {
- id: 'legal-notice',
- name: intl.formatMessage({
- defaultMessage: 'Legal notice',
- description: 'FooterNav: legal notice link',
- id: 'yWjXRx',
- }),
- slug: '/mentions-legales',
- },
- ];
-
- const navItems = footerNavConfig.map((item) => {
- return (
- <li key={item.id} className={styles.item}>
- <Link href={item.slug}>
- <a className={styles.link}>{item.name}</a>
- </Link>
- </li>
- );
- });
-
- return (
- <nav
- className={styles.nav}
- aria-label={intl.formatMessage({
- defaultMessage: 'Footer',
- description: 'FooterNav: aria-label',
- id: 'HTdaZj',
- })}
- >
- <ul className={styles.list}>{navItems}</ul>
- </nav>
- );
-};
-
-export default FooterNav;
diff --git a/src/components/FormElements/Field/Field.module.scss b/src/components/FormElements/Field/Field.module.scss
deleted file mode 100644
index 9100495..0000000
--- a/src/components/FormElements/Field/Field.module.scss
+++ /dev/null
@@ -1,53 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.field {
- background: var(--color-bg-tertiary);
- border: fun.convert-px(2) solid var(--color-border);
- box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow);
- transition: all 0.25s linear 0s;
-
- &:not(.select) {
- width: 100%;
- padding: var(--spacing-2xs) var(--spacing-xs);
- }
-
- &:hover {
- box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1)
- var(--color-shadow);
- transform: translate(#{fun.convert-px(-3)}, #{fun.convert-px(-3)});
- }
-
- &:focus {
- background: var(--color-bg);
- border-color: var(--color-primary);
- box-shadow: 0 0 0 0 var(--color-shadow);
- transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
- outline: none;
- transition: all 0.2s ease-in-out 0s, transform 0.3s ease-out 0s;
- }
-}
-
-.select {
- padding: var(--spacing-2xs) var(--spacing-xs);
- cursor: pointer;
-
- @include mix.pointer("fine") {
- padding: fun.convert-px(3) var(--spacing-xs);
- }
-
- &:hover {
- box-shadow: fun.convert-px(4) fun.convert-px(4) 0 fun.convert-px(1)
- var(--color-shadow);
- transform: translate(#{fun.convert-px(-2)}, #{fun.convert-px(-2)});
- }
-
- &:focus {
- box-shadow: 0 0 0 0 var(--color-shadow);
- transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
- }
-}
-
-.textarea {
- min-height: fun.convert-px(200);
-}
diff --git a/src/components/FormElements/Field/Field.tsx b/src/components/FormElements/Field/Field.tsx
deleted file mode 100644
index c8df0f9..0000000
--- a/src/components/FormElements/Field/Field.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import {
- ChangeEvent,
- ForwardedRef,
- forwardRef,
- ReactElement,
- SetStateAction,
-} from 'react';
-import styles from './Field.module.scss';
-
-type FieldType = 'email' | 'number' | 'search' | 'select' | 'text' | 'textarea';
-type SelectOptions = {
- id: string;
- name: string;
- value: string;
-};
-
-const Field = (
- {
- id,
- name,
- value,
- setValue,
- required = false,
- kind = 'text',
- label,
- options,
- }: {
- id: string;
- name: string;
- value: string;
- setValue: (value: SetStateAction<string>) => void;
- required?: boolean;
- kind?: FieldType;
- label?: ReactElement;
- options?: SelectOptions[];
- },
- ref: ForwardedRef<HTMLInputElement | HTMLTextAreaElement>
-) => {
- const updateValue = (
- e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
- ) => {
- setValue(e.target.value);
- };
-
- const getOptions = () => {
- return options
- ? options.map((option) => (
- <option key={option.id} value={option.value}>
- {option.name}
- </option>
- ))
- : '';
- };
-
- const getField = () => {
- switch (kind) {
- case 'select':
- return (
- <select
- name={name}
- id={id}
- value={value}
- onChange={updateValue}
- required={required}
- className={`${styles.field} ${styles.select}`}
- >
- {getOptions()}
- </select>
- );
- case 'textarea':
- return (
- <textarea
- ref={ref as ForwardedRef<HTMLTextAreaElement>}
- id={id}
- name={name}
- value={value}
- required={required}
- onChange={updateValue}
- className={`${styles.field} ${styles.textarea}`}
- />
- );
- default:
- return (
- <input
- ref={ref as ForwardedRef<HTMLInputElement>}
- type={kind}
- id={id}
- name={name}
- value={value}
- required={required}
- onChange={updateValue}
- className={styles.field}
- />
- );
- }
- };
-
- return (
- <>
- {label}
- {getField()}
- </>
- );
-};
-
-export default forwardRef(Field);
diff --git a/src/components/FormElements/Form/Form.module.scss b/src/components/FormElements/Form/Form.module.scss
deleted file mode 100644
index 0f7c437..0000000
--- a/src/components/FormElements/Form/Form.module.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.wrapper {
- width: 100%;
-}
-
-.centered {
- max-width: 45ch;
- margin-left: auto;
- margin-right: auto;
-}
-
-.search {
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
-
- > input {
- padding-right: calc(var(--btn-size) + var(--spacing-2xs));
-
- &:hover ~ button {
- transform: translate(fun.convert-px(-3), fun.convert-px(-3));
- }
-
- &:focus ~ button {
- transform: translate(fun.convert-px(3), fun.convert-px(3));
- }
- }
-}
-
-.settings {
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- margin: var(--spacing-sm) 0;
- position: relative;
-}
diff --git a/src/components/FormElements/Form/Form.tsx b/src/components/FormElements/Form/Form.tsx
deleted file mode 100644
index 10fdcdf..0000000
--- a/src/components/FormElements/Form/Form.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ReactNode } from 'react';
-import styles from './Form.module.scss';
-
-type FormKind = 'centered' | 'search' | 'settings';
-
-const Form = ({
- children,
- submitHandler,
- kind,
- id,
-}: {
- children: ReactNode;
- submitHandler: any;
- kind?: FormKind;
- id?: string;
-}) => {
- const kindStyles = kind ? styles[kind] : '';
- const classes = `${styles.wrapper} ${kindStyles}`;
-
- return (
- <form onSubmit={submitHandler} className={classes} id={id}>
- {children}
- </form>
- );
-};
-
-export default Form;
diff --git a/src/components/FormElements/FormItem/FormItem.module.scss b/src/components/FormElements/FormItem/FormItem.module.scss
deleted file mode 100644
index 07ef56f..0000000
--- a/src/components/FormElements/FormItem/FormItem.module.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.wrapper {
- margin: var(--spacing-xs) 0;
- max-width: 45ch;
-}
diff --git a/src/components/FormElements/FormItem/FormItem.tsx b/src/components/FormElements/FormItem/FormItem.tsx
deleted file mode 100644
index 8d674f1..0000000
--- a/src/components/FormElements/FormItem/FormItem.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import styles from './FormItem.module.scss';
-
-const FormItem: React.FunctionComponent = ({ children }) => {
- return <div className={styles.wrapper}>{children}</div>;
-};
-
-export default FormItem;
diff --git a/src/components/FormElements/Label/Label.module.scss b/src/components/FormElements/Label/Label.module.scss
deleted file mode 100644
index c527b16..0000000
--- a/src/components/FormElements/Label/Label.module.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.regular {
- display: block;
- color: var(--color-primary-darker);
- font-size: var(--font-size-sm);
- font-variant: small-caps;
- font-weight: 600;
-}
-
-.settings {
- --icon-size: #{fun.convert-px(25)};
- --toggle-width: #{fun.convert-px(45)};
- --toggle-height: calc(var(--toggle-width) / 2);
-
- display: inline-flex;
- align-items: center;
-}
-
-.required {
- color: var(--color-secondary);
-}
diff --git a/src/components/FormElements/Label/Label.tsx b/src/components/FormElements/Label/Label.tsx
deleted file mode 100644
index baedff0..0000000
--- a/src/components/FormElements/Label/Label.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import styles from './Label.module.scss';
-
-type LabelKind = 'regular' | 'settings';
-
-const Label = ({
- body,
- htmlFor,
- required = false,
- kind = 'regular',
-}: {
- body: string;
- htmlFor: string;
- required?: boolean;
- kind?: LabelKind;
-}) => {
- return (
- <label htmlFor={htmlFor} className={styles[kind]}>
- {body}
- {required && <span className={styles.required}> *</span>}
- </label>
- );
-};
-
-export default Label;
diff --git a/src/components/FormElements/Toggle/Toggle.tsx b/src/components/FormElements/Toggle/Toggle.tsx
deleted file mode 100644
index 4db7d43..0000000
--- a/src/components/FormElements/Toggle/Toggle.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { FormEvent, ReactElement } from 'react';
-import { Form } from '..';
-import styles from './Toggle.module.scss';
-
-const Toggle = ({
- id,
- label,
- value,
- changeHandler,
- leftChoice,
- rightChoice,
- name,
-}: {
- id: string;
- label: string;
- value: boolean;
- changeHandler: (value: boolean) => void;
- leftChoice: ReactElement | string;
- rightChoice: ReactElement | string;
- name?: string;
-}) => {
- const onSubmit = (e: FormEvent) => {
- e.preventDefault();
- };
-
- return (
- <Form kind="settings" submitHandler={onSubmit}>
- <input
- className={styles.checkbox}
- type="checkbox"
- id={id}
- name={name ? name : id}
- checked={value}
- onChange={() => changeHandler(!value)}
- />
- <label htmlFor={id} className={styles.label}>
- <span className={styles.title}>{label}</span>
- {leftChoice}
- <span className={styles.toggle}></span>
- {rightChoice}
- </label>
- </Form>
- );
-};
-
-export default Toggle;
diff --git a/src/components/FormElements/index.tsx b/src/components/FormElements/index.tsx
deleted file mode 100644
index 8ca69b4..0000000
--- a/src/components/FormElements/index.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import Field from './Field/Field';
-import Form from './Form/Form';
-import FormItem from './FormItem/FormItem';
-import Label from './Label/Label';
-import Toggle from './Toggle/Toggle';
-
-export { Field, Form, FormItem, Label, Toggle };
diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss
deleted file mode 100644
index aa0d8cf..0000000
--- a/src/components/Header/Header.module.scss
+++ /dev/null
@@ -1,22 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.wrapper {
- display: grid;
- grid-template-columns:
- minmax(0, 1fr) min(calc(100vw - calc(var(--spacing-md) * 2)), 100ch)
- minmax(0, 1fr);
- align-items: center;
- padding: var(--spacing-sm) 0 var(--spacing-md);
- position: relative;
- background: var(--color-bg);
- border-bottom: fun.convert-px(3) solid var(--color-border-light);
-}
-
-.body {
- grid-column: 2;
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: space-between;
- gap: var(--spacing-md);
-}
diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx
deleted file mode 100644
index 0b773e9..0000000
--- a/src/components/Header/Header.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import Branding from '@components/Branding/Branding';
-import Toolbar from '@components/Toolbar/Toolbar';
-import styles from './Header.module.scss';
-
-const Header = ({ isHome }: { isHome: boolean }) => {
- return (
- <header id="top" className={styles.wrapper}>
- <div className={styles.body}>
- <Branding isHome={isHome} />
- <Toolbar />
- </div>
- </header>
- );
-};
-
-export default Header;
diff --git a/src/components/Icons/Arrow/Arrow.module.scss b/src/components/Icons/Arrow/Arrow.module.scss
deleted file mode 100644
index 49e9b02..0000000
--- a/src/components/Icons/Arrow/Arrow.module.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.icon {
- fill: var(--color-primary);
- width: var(--icon-size, #{fun.convert-px(30)});
- transition: all 0.25s ease-in-out 0s;
-}
diff --git a/src/components/Icons/Hamburger/Hamburger.tsx b/src/components/Icons/Hamburger/Hamburger.tsx
deleted file mode 100644
index 9b39272..0000000
--- a/src/components/Icons/Hamburger/Hamburger.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import styles from './Hamburger.module.scss';
-
-const HamburgerIcon = ({ isActive }: { isActive: boolean }) => {
- const withModifier = isActive ? ` ${styles['icon--active']}` : '';
- const iconClasses = `${styles.icon} ${withModifier}`;
-
- return <span className={iconClasses}></span>;
-};
-
-export default HamburgerIcon;
diff --git a/src/components/Icons/index.tsx b/src/components/Icons/index.tsx
deleted file mode 100644
index 5fe2c19..0000000
--- a/src/components/Icons/index.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import ArrowIcon from './Arrow/Arrow';
-import BlogIcon from './Blog/Blog';
-import CloseIcon from './Close/Close';
-import CogIcon from './Cog/Cog';
-import ContactIcon from './Contact/Contact';
-import CopyrightIcon from './Copyright/Copyright';
-import CVIcon from './CV/CV';
-import HamburgerIcon from './Hamburger/Hamburger';
-import HomeIcon from './Home/Home';
-import MoonIcon from './Moon/Moon';
-import ProjectsIcon from './Projects/Projects';
-import SearchIcon from './Search/Search';
-import SunIcon from './Sun/Sun';
-
-export {
- ArrowIcon,
- BlogIcon,
- CloseIcon,
- CogIcon,
- ContactIcon,
- CopyrightIcon,
- CVIcon,
- HamburgerIcon,
- HomeIcon,
- MoonIcon,
- ProjectsIcon,
- SearchIcon,
- SunIcon,
-};
diff --git a/src/components/Layouts/Layout.module.scss b/src/components/Layouts/Layout.module.scss
deleted file mode 100644
index 33339d4..0000000
--- a/src/components/Layouts/Layout.module.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.noscript {
- width: 100%;
- padding: var(--spacing-xs) var(--spacing-sm);
- position: fixed;
- top: 0;
- z-index: 10;
- background: var(--color-bg);
- border-bottom: fun.convert-px(3) solid var(--color-border);
- color: var(--color-primary-darker);
- font-size: var(--font-size-sm);
- font-weight: 600;
- text-align: center;
-}
-
-.noscript-spacing {
- width: 100%;
- height: fun.convert-px(80);
-}
diff --git a/src/components/Layouts/Layout.tsx b/src/components/Layouts/Layout.tsx
deleted file mode 100644
index ada32b3..0000000
--- a/src/components/Layouts/Layout.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import Footer from '@components/Footer/Footer';
-import Header from '@components/Header/Header';
-import Main from '@components/Main/Main';
-import Breadcrumb from '@components/Breadcrumb/Breadcrumb';
-import { settings } from '@utils/config';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
-import { ReactElement, ReactNode, useEffect, useRef } from 'react';
-import { useIntl } from 'react-intl';
-import { SearchAction, WebSite, WithContext } from 'schema-dts';
-import styles from './Layout.module.scss';
-import Script from 'next/script';
-
-const Layout = ({
- children,
- isHome = false,
-}: {
- children: ReactNode;
- isHome?: boolean;
-}) => {
- const intl = useIntl();
- const { asPath, locale } = useRouter();
- const ref = useRef<HTMLSpanElement>(null);
-
- useEffect(() => {
- ref.current?.focus();
- }, [asPath]);
-
- type QueryAction = SearchAction & {
- 'query-input': string;
- };
-
- const searchActionSchema: QueryAction = {
- '@type': 'SearchAction',
- target: {
- '@type': 'EntryPoint',
- urlTemplate: `${settings.url}/recherche?s={search_term_string}`,
- },
- query: 'required',
- 'query-input': 'required name=search_term_string',
- };
-
- const schemaJsonLd: WithContext<WebSite> = {
- '@context': 'https://schema.org',
- '@id': `${settings.url}`,
- '@type': 'WebSite',
- name: settings.name,
- description: locale?.startsWith('en')
- ? settings.baseline.en
- : settings.baseline.fr,
- url: settings.url,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: Number(settings.copyright.startYear),
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- potentialAction: searchActionSchema,
- };
-
- return (
- <>
- <Head>
- <meta property="og:site_name" content={settings.name} />
- <meta
- property="og:locale"
- content={`${settings.locales.defaultLocale}_${settings.locales.defaultCountry}`}
- />
- <meta property="twitter:card" content="summary" />
- <meta property="twitter:site" content={settings.twitterId} />
- <meta property="twitter:creator" content={settings.twitterId} />
- <meta
- name="theme-color"
- content="#14578a"
- media="(prefers-color-scheme: light)"
- />
- <meta
- name="theme-color"
- content="#85bbd6"
- media="(prefers-color-scheme: dark)"
- />
- <link
- rel="alternate"
- href="/feed.xml"
- type="application/rss+xml"
- title={`${settings.name}'s RSS feed`}
- />
- <link
- rel="alternate"
- href="/atom.xml"
- type="application/atom+xml"
- title={`${settings.name}'s Atom feed`}
- />
- <link
- rel="alternate"
- href="/feed.json"
- type="application/feed+json"
- title={`${settings.name}'s Json feed`}
- />
- <link rel="icon" href="/favicon.ico" sizes="any" />
- <link rel="icon" href="/icon.svg" type="image/svg+xml" />
- <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
- <link rel="manifest" href="/manifest.webmanifest" />
- </Head>
- <Script
- id="schema-layout"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- ></Script>
- <Script
- strategy="afterInteractive"
- async
- src={`${settings.ackee.url}/${settings.ackee.filename}`}
- data-ackee-server={settings.ackee.url}
- data-ackee-domain-id={settings.ackee.siteId}
- />
- <noscript>
- <div className={styles['noscript-spacing']}></div>
- </noscript>
- <span ref={ref} tabIndex={-1} />
- <a href="#main" className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'Skip to content',
- description: 'Layout: Skip to content button',
- id: 'iqAbyn',
- })}
- </a>
- <Header isHome={isHome} />
- <Main>{children}</Main>
- <Footer />
- <noscript>
- <div className={styles.noscript}>
- {intl.formatMessage({
- defaultMessage:
- 'Without Javascript, some features may not work like loading more posts or use search. If you want to benefit from these features, please activate Javascript.',
- description: 'Layout: noscript banner',
- id: 'LR70nt',
- })}
- </div>
- </noscript>
- </>
- );
-};
-
-export const getLayout = (page: ReactElement) => {
- const pageTitle: string = page.props.breadcrumbTitle;
-
- return (
- <Layout>
- <Breadcrumb pageTitle={pageTitle} />
- {page}
- </Layout>
- );
-};
-
-export default Layout;
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx
deleted file mode 100644
index c330063..0000000
--- a/src/components/MDX/CodeBlock/CodeBlock.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import {
- PrismDefaultPlugins,
- PrismLanguages,
- PrismPlugins,
-} from '@ts/types/prism';
-import { usePrismTheme } from '@utils/providers/prism-theme';
-import { useRouter } from 'next/router';
-import Prism from 'prismjs';
-import { useCallback, useEffect, useMemo } from 'react';
-import { useIntl } from 'react-intl';
-
-const CodeBlock = ({
- code,
- language,
- plugins,
-}: {
- code: string;
- language: PrismLanguages;
- plugins: PrismPlugins[];
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const { setCodeBlocks } = usePrismTheme();
-
- useEffect(() => {
- const allPre: NodeListOf<HTMLPreElement> = document.querySelectorAll(
- 'pre[data-prismjs-color-scheme-current]'
- );
- setCodeBlocks(allPre);
- }, [setCodeBlocks, router.asPath]);
-
- const defaultPlugins: PrismDefaultPlugins[] = useMemo(
- () => [
- 'autoloader',
- 'toolbar',
- 'show-language',
- 'copy-to-clipboard',
- 'color-scheme',
- 'match-braces',
- 'normalize-whitespace',
- ],
- []
- );
-
- const loadPrismPlugins = useCallback(
- async (prismPlugins: (PrismDefaultPlugins | PrismPlugins)[]) => {
- for (const plugin of prismPlugins) {
- try {
- if (plugin === 'color-scheme') {
- await import(`@utils/plugins/prism-${plugin}`);
- } else {
- await import(`prismjs/plugins/${plugin}/prism-${plugin}.min.js`);
-
- if (plugin === 'autoloader')
- Prism.plugins.autoloader.languages_path = '/prism/';
- }
- } catch (error) {
- console.error('CodeBlock: an error occurred with Prism.');
- console.error(error);
- }
- }
- },
- []
- );
-
- useEffect(() => {
- loadPrismPlugins([...defaultPlugins, ...plugins]).then(() => {
- Prism.highlightAll();
- });
- }, [loadPrismPlugins, defaultPlugins, plugins]);
-
- const copyText = intl.formatMessage({
- defaultMessage: 'Copy',
- description: 'Prism: copy button text (no clicked)',
- id: '/ly3AC',
- });
- const copiedText = intl.formatMessage({
- defaultMessage: 'Copied!',
- description: 'Prism: copy button text (clicked)',
- id: 'OV9r1K',
- });
- const errorText = intl.formatMessage({
- defaultMessage: 'Use Ctrl+c to copy',
- description: 'Prism: error text',
- id: 'z9qkcQ',
- });
- const darkTheme = intl.formatMessage({
- defaultMessage: 'Dark Theme 🌙',
- description: 'Prism: toggle dark theme button text',
- id: 'nFMdWI',
- });
- const lightTheme = intl.formatMessage({
- defaultMessage: 'Light Theme 🌞',
- description: 'Prism: toggle light theme button text',
- id: 'Ua2g2p',
- });
-
- const defaultPluginsClasses = 'match-braces';
- const pluginsClasses = plugins.join(' ');
-
- return (
- <pre
- className={`language-${language} ${defaultPluginsClasses} ${pluginsClasses}`}
- data-prismjs-copy={copyText}
- data-prismjs-copy-success={copiedText}
- data-prismjs-copy-error={errorText}
- data-prismjs-color-scheme-dark={darkTheme}
- data-prismjs-color-scheme-light={lightTheme}
- >
- <code className={`language-${language}`}>{code}</code>
- </pre>
- );
-};
-
-export default CodeBlock;
diff --git a/src/components/MDX/Gallery/Gallery.tsx b/src/components/MDX/Gallery/Gallery.tsx
deleted file mode 100644
index 561ec53..0000000
--- a/src/components/MDX/Gallery/Gallery.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Children, ReactElement } from 'react';
-import styles from './Gallery.module.scss';
-
-const Gallery = ({
- children,
- columns = 2,
-}: {
- children: ReactElement;
- columns: number;
-}) => {
- const columnClass = styles[`wrapper--${columns}-columns`];
-
- return (
- <ul className={`${styles.wrapper} ${columnClass}`}>
- {Children.map(children, (child) => {
- return <li className={styles.item}>{child}</li>;
- })}
- </ul>
- );
-};
-
-export default Gallery;
diff --git a/src/components/MDX/Link/Link.tsx b/src/components/MDX/Link/Link.tsx
deleted file mode 100644
index 40e773b..0000000
--- a/src/components/MDX/Link/Link.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ReactChildren } from 'react';
-
-const Link = ({
- children,
- target,
- isExternal = false,
- lang,
-}: {
- children: ReactChildren;
- target: string;
- isExternal: boolean;
- lang?: string;
-}) => {
- const className = isExternal ? 'external' : '';
-
- return (
- <a href={target} className={className} hrefLang={lang}>
- {children}
- </a>
- );
-};
-
-export default Link;
diff --git a/src/components/MDX/ResponsiveImage/ResponsiveImage.module.scss b/src/components/MDX/ResponsiveImage/ResponsiveImage.module.scss
deleted file mode 100644
index cf2b77f..0000000
--- a/src/components/MDX/ResponsiveImage/ResponsiveImage.module.scss
+++ /dev/null
@@ -1,50 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.wrapper {
- width: 100%;
- max-width: 100%;
- margin: var(--spacing-sm) auto;
- position: relative;
- text-align: center;
-}
-
-.caption {
- margin: 0;
- padding: fun.convert-px(4) var(--spacing-2xs);
- background: var(--color-bg-secondary);
- border: fun.convert-px(1) solid var(--color-border);
- box-shadow: 0 fun.convert-px(-1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light);
- font-weight: 500;
-}
-
-.link {
- display: flex;
- flex-flow: column;
- background: none;
- text-decoration: none;
-
- .caption {
- color: var(--color-primary-darker);
- }
-
- &:hover,
- &:focus {
- transform: scale(1.1);
- }
-
- &:focus {
- .caption {
- text-decoration: underline solid var(--color-primary-darker)
- fun.convert-px(3);
- }
- }
-
- &:active {
- transform: scale(0.9);
-
- .caption {
- text-decoration: none;
- }
- }
-}
diff --git a/src/components/MDX/ResponsiveImage/ResponsiveImage.tsx b/src/components/MDX/ResponsiveImage/ResponsiveImage.tsx
deleted file mode 100644
index 6c39e7f..0000000
--- a/src/components/MDX/ResponsiveImage/ResponsiveImage.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { ResponsiveImageProps } from '@ts/types/app';
-import Image from 'next/image';
-import Link from 'next/link';
-import styles from './ResponsiveImage.module.scss';
-
-const ResponsiveImage = (props: ResponsiveImageProps) => {
- const { caption, linkTarget, ...attributes } = props;
-
- return (
- <figure className={styles.wrapper}>
- {linkTarget ? (
- <Link href={linkTarget}>
- <a className={styles.link}>
- <Image
- alt={attributes.alt}
- layout={attributes.layout || 'intrinsic'}
- {...attributes}
- />
- {caption && (
- <figcaption className={styles.caption}>{caption}</figcaption>
- )}
- </a>
- </Link>
- ) : (
- <>
- <Image
- alt={attributes.alt}
- layout={attributes.layout || 'intrinsic'}
- {...attributes}
- />
- {caption && (
- <figcaption className={styles.caption}>{caption}</figcaption>
- )}
- </>
- )}
- </figure>
- );
-};
-
-export default ResponsiveImage;
diff --git a/src/components/MDX/index.tsx b/src/components/MDX/index.tsx
deleted file mode 100644
index bc7aa35..0000000
--- a/src/components/MDX/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import CodeBlock from './CodeBlock/CodeBlock';
-import Gallery from './Gallery/Gallery';
-import Link from './Link/Link';
-import ResponsiveImage from './ResponsiveImage/ResponsiveImage';
-
-export { CodeBlock, Gallery, Link, ResponsiveImage };
diff --git a/src/components/Main/Main.module.scss b/src/components/Main/Main.module.scss
deleted file mode 100644
index 819474c..0000000
--- a/src/components/Main/Main.module.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.wrapper {
- flex: 1;
-
- :global {
- animation: fade-in 1.5s ease-in-out 0s 1;
- }
-}
diff --git a/src/components/Main/Main.tsx b/src/components/Main/Main.tsx
deleted file mode 100644
index b21ab1c..0000000
--- a/src/components/Main/Main.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { FunctionComponent } from 'react';
-import styles from './Main.module.scss';
-
-const Main: FunctionComponent = ({ children }) => {
- return (
- <main id="main" className={styles.wrapper}>
- {children}
- </main>
- );
-};
-
-export default Main;
diff --git a/src/components/MainNav/MainNav.module.scss b/src/components/MainNav/MainNav.module.scss
deleted file mode 100644
index f3e6c10..0000000
--- a/src/components/MainNav/MainNav.module.scss
+++ /dev/null
@@ -1,242 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-@use "@styles/abstracts/placeholders";
-
-.wrapper {
- --icon-size: #{fun.convert-px(30)};
-
- display: flex;
- flex-flow: column nowrap;
- align-items: center;
- height: var(--btn-size);
- width: calc(var(--btn-size) * 1.2);
- background: var(--color-bg);
- position: relative;
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- background: inherit;
- }
-
- @include mix.dimensions("md") {
- width: unset;
- height: unset;
- }
- }
-}
-
-.label {
- --draw-border-thickness: #{fun.convert-px(5)};
- --draw-border-color1: var(--color-primary-light);
- --draw-border-color2: var(--color-primary-lighter);
-
- flex: 1;
- display: flex;
- flex-flow: column nowrap;
- width: 100%;
- padding: var(--spacing-2xs);
-
- &:hover {
- @extend %draw-borders;
- }
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- display: none;
- }
- }
-}
-
-.checkbox {
- position: absolute;
-
- // centered checkbox = btn-size - approximated checkbox size / 2
- top: calc((var(--btn-size) - #{fun.convert-px(14)}) / 2);
- left: calc(((var(--btn-size) * 1.2) - #{fun.convert-px(14)}) / 2);
- opacity: 0;
- cursor: pointer;
-
- &:hover {
- ~ .label {
- @extend %draw-borders;
- }
- }
-
- &:focus {
- ~ .label {
- @extend %draw-borders;
- }
- }
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- display: none;
- }
- }
-}
-
-.nav {
- display: flex;
- flex-flow: column wrap;
- place-content: center;
- padding-bottom: var(--toolbar-size);
- position: fixed;
- bottom: 0;
- z-index: -1;
- background: var(--color-bg-opacity);
- box-shadow: 0 0 fun.convert-px(3) 0 var(--color-shadow-dark);
- text-align: center;
- opacity: 1;
- visibility: visible;
- transform: translateY(0);
- transition: all 0.8s ease-in-out 0s;
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- padding-bottom: 0;
- position: absolute;
- bottom: auto;
- left: auto;
- right: auto;
- top: calc(var(--btn-size) + var(--spacing-sm));
- z-index: unset;
- border-bottom-width: fun.convert-px(5);
- transform-origin: 50% -100%;
- }
-
- @include mix.dimensions("md") {
- background: transparent;
- border: none;
- box-shadow: none;
- position: relative;
- top: 0;
- }
- }
-}
-
-.list {
- @extend %reset-list;
-
- @include mix.media("screen") {
- @include mix.dimensions(null, "2xs", "height") {
- display: grid;
- grid-template-columns: min-content min-content;
- max-height: calc(100vh - var(--toolbar-size));
- }
-
- @include mix.dimensions("md") {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- gap: var(--spacing-2xs);
- }
- }
-}
-
-.link {
- --draw-border-thickness: #{fun.convert-px(4)};
- --draw-border-color1: var(--color-primary-light);
- --draw-border-color2: var(--color-primary-lighter);
-
- display: block;
- min-width: fun.convert-px(85);
- padding: var(--spacing-xs) var(--spacing-xs) var(--spacing-2xs);
- background: var(--color-bg);
- background-repeat: no-repeat;
- font-size: var(--font-size-sm);
- font-variant: small-caps;
- font-weight: 600;
- text-decoration: none;
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- margin: 0;
- background-color: inherit;
- border-radius: 8%;
- }
- }
-
- &:hover,
- &:focus {
- @extend %draw-borders;
- }
-
- &:focus {
- color: var(--color-primary-light);
- }
-
- &:active {
- --draw-border-color1: var(--color-primary-dark);
- --draw-border-color2: var(--color-primary-light);
-
- @extend %draw-borders;
- }
-
- &.current {
- background-image: linear-gradient(to right, transparent, transparent),
- linear-gradient(to bottom, transparent, transparent),
- linear-gradient(
- to left,
- var(--color-primary-lighter),
- var(--color-primary-light)
- ),
- linear-gradient(to top, transparent, transparent);
- background-position: top left, top right, bottom center, bottom left;
- background-size: 0% var(--draw-border-thickness),
- var(--draw-border-thickness) 0%, 60% var(--draw-border-thickness),
- var(--draw-border-thickness) 0%;
-
- &:hover,
- &:focus {
- --draw-border-color1: var(--color-primary-light);
- --draw-border-color2: var(--color-primary-lighter);
-
- @extend %draw-borders;
- }
-
- &:active {
- --draw-border-color1: var(--color-primary-dark);
- --draw-border-color2: var(--color-primary-light);
-
- @extend %draw-borders;
- }
- }
-}
-
-.checkbox:not(:checked) {
- ~ .nav {
- opacity: 0;
- visibility: hidden;
- transform: translateY(100vw);
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- transform: perspective(20rem) translate3d(0, 100%, -20rem);
- }
-
- @include mix.dimensions("md") {
- opacity: 1;
- visibility: visible;
- transform: none;
- }
- }
- }
-}
-
-.checkbox:checked {
- ~ .label:hover {
- span {
- background: none;
- box-shadow: none;
- }
- }
-
- &:hover {
- ~ .label {
- span {
- background: none;
- box-shadow: none;
- }
- }
- }
-}
diff --git a/src/components/MainNav/MainNav.tsx b/src/components/MainNav/MainNav.tsx
deleted file mode 100644
index 9cb6b4c..0000000
--- a/src/components/MainNav/MainNav.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import {
- BlogIcon,
- ContactIcon,
- CVIcon,
- HamburgerIcon,
- HomeIcon,
- ProjectsIcon,
-} from '@components/Icons';
-import { NavItem } from '@ts/types/nav';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import { ForwardedRef, forwardRef, SetStateAction } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './MainNav.module.scss';
-
-const MainNav = (
- {
- isOpened,
- setIsOpened,
- }: {
- isOpened: boolean;
- setIsOpened: (value: SetStateAction<boolean>) => void;
- },
- ref: ForwardedRef<HTMLDivElement>
-) => {
- const intl = useIntl();
- const router = useRouter();
-
- const mainNavConfig: NavItem[] = [
- {
- id: 'home',
- name: intl.formatMessage({
- defaultMessage: 'Home',
- description: 'MainNav: home link',
- id: 'ZJMNRW',
- }),
- slug: '/',
- },
- {
- id: 'blog',
- name: intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'MainNav: blog link',
- id: 'zPJifH',
- }),
- slug: '/blog',
- },
- {
- id: 'projects',
- name: intl.formatMessage({
- defaultMessage: 'Projects',
- description: 'MainNav: projects link',
- id: 'akSutM',
- }),
- slug: '/projets',
- },
- {
- id: 'cv',
- name: intl.formatMessage({
- defaultMessage: 'Resume',
- description: 'MainNav: resume link',
- id: 'jpv+Nz',
- }),
- slug: '/cv',
- },
- {
- id: 'contact',
- name: intl.formatMessage({
- defaultMessage: 'Contact',
- description: 'MainNav: contact link',
- id: 'c2NtPj',
- }),
- slug: '/contact',
- },
- ];
-
- const getIcon = (id: string) => {
- switch (id) {
- case 'home':
- return <HomeIcon />;
- case 'blog':
- return <BlogIcon />;
- case 'contact':
- return <ContactIcon />;
- case 'cv':
- return <CVIcon />;
- case 'projects':
- return <ProjectsIcon />;
- default:
- break;
- }
- };
-
- const navItems = mainNavConfig.map((item) => {
- const currentClass = router.asPath === item.slug ? styles.current : '';
-
- return (
- <li key={item.id}>
- <Link href={item.slug}>
- <a className={`${styles.link} ${currentClass}`}>
- {getIcon(item.id)}
- <span>{item.name}</span>
- </a>
- </Link>
- </li>
- );
- });
-
- return (
- <div id="main-nav" ref={ref} className={styles.wrapper}>
- <input
- type="checkbox"
- name="main-nav__checkbox"
- id="main-nav__checkbox"
- aria-labelledby="main-nav-toggle"
- className={styles.checkbox}
- checked={isOpened}
- onChange={() => setIsOpened(!isOpened)}
- autoComplete="off"
- />
- <label
- htmlFor="main-nav__checkbox"
- id="main-nav-toggle"
- className={styles.label}
- >
- <HamburgerIcon isActive={isOpened} />
- <span className="screen-reader-text">
- {isOpened
- ? intl.formatMessage({
- defaultMessage: 'Close menu',
- description: 'MainNav: close button',
- id: 'dE8xxV',
- })
- : intl.formatMessage({
- defaultMessage: 'Open menu',
- description: 'MainNav: open button',
- id: 'azc1GT',
- })}
- </span>
- </label>
- <nav
- className={styles.nav}
- aria-label={intl.formatMessage({
- defaultMessage: 'Primary',
- description: 'MainNav: aria-label',
- id: 'H7C5Bk',
- })}
- >
- <ul className={styles.list}>{navItems}</ul>
- </nav>
- </div>
- );
-};
-
-export default forwardRef(MainNav);
diff --git a/src/components/MetaItems/Author/Author.tsx b/src/components/MetaItems/Author/Author.tsx
deleted file mode 100644
index 4ff0086..0000000
--- a/src/components/MetaItems/Author/Author.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const Author = ({ name, kind }: { name: string; kind: MetaKind }) => {
- const intl = useIntl();
-
- return (
- <MetaItem
- title={intl.formatMessage({
- defaultMessage: 'Written by:',
- description: 'Author: article author meta label',
- id: 'jCyqZS',
- })}
- value={name}
- kind={kind}
- />
- );
-};
-
-export default Author;
diff --git a/src/components/MetaItems/CommentsCount/CommentsCount.tsx b/src/components/MetaItems/CommentsCount/CommentsCount.tsx
deleted file mode 100644
index 04cffa6..0000000
--- a/src/components/MetaItems/CommentsCount/CommentsCount.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const CommentsCount = ({ total, kind }: { total: number; kind: MetaKind }) => {
- const intl = useIntl();
- const { asPath } = useRouter();
-
- const isArticle = () => asPath.includes('/article/');
-
- const getCommentsCount = () => {
- return intl.formatMessage(
- {
- defaultMessage:
- '{total, plural, =0 {No comments} one {# comment} other {# comments}}',
- description: 'CommentsCount: comment count value',
- id: 'lKGNKx',
- },
- { total }
- );
- };
-
- return (
- <MetaItem
- title={intl.formatMessage({
- defaultMessage: 'Comments:',
- description: 'CommentsCount: comment count meta label',
- id: '6BRtAu',
- })}
- value={
- isArticle() ? (
- <a href="#comments">{getCommentsCount()}</a>
- ) : (
- getCommentsCount()
- )
- }
- kind={kind}
- />
- );
-};
-
-export default CommentsCount;
diff --git a/src/components/MetaItems/Dates/Dates.tsx b/src/components/MetaItems/Dates/Dates.tsx
deleted file mode 100644
index 4314ed9..0000000
--- a/src/components/MetaItems/Dates/Dates.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { getFormattedDate } from '@utils/helpers/format';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const Dates = ({
- publication,
- update,
- kind,
-}: {
- publication: string;
- update: string;
- kind: MetaKind;
-}) => {
- const intl = useIntl();
- const { locale } = useRouter();
- const validLocale = locale ? locale : settings.locales.defaultLocale;
-
- const publicationDate = getFormattedDate(publication, validLocale);
- const updateDate = getFormattedDate(update, validLocale);
-
- return (
- <>
- <MetaItem
- title={intl.formatMessage({
- defaultMessage: 'Published on:',
- description: 'Dates: publication date meta label',
- id: '52Fev1',
- })}
- values={[
- <time key={publication} dateTime={publication}>
- {publicationDate}
- </time>,
- ]}
- kind={kind}
- />
- {publicationDate !== updateDate && (
- <MetaItem
- title={intl.formatMessage({
- defaultMessage: 'Updated on:',
- description: 'Dates: update date meta label',
- id: 'C+r/LF',
- })}
- values={[
- <time key={update} dateTime={update}>
- {updateDate}
- </time>,
- ]}
- kind={kind}
- />
- )}
- </>
- );
-};
-
-export default Dates;
diff --git a/src/components/MetaItems/MetaItem/MetaItem.module.scss b/src/components/MetaItems/MetaItem/MetaItem.module.scss
deleted file mode 100644
index 0b159ca..0000000
--- a/src/components/MetaItems/MetaItem/MetaItem.module.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-.wrapper--article {
- display: flex;
- flex-flow: row wrap;
-}
-
-.title--article {
- margin-right: var(--spacing-2xs);
- color: var(--color-fg-light);
-}
-
-.body--article {
- &:not(:first-of-type) {
- &::before {
- content: "/";
- margin: 0 var(--spacing-2xs);
- }
- }
-}
diff --git a/src/components/MetaItems/MetaItem/MetaItem.tsx b/src/components/MetaItems/MetaItem/MetaItem.tsx
deleted file mode 100644
index 5c51283..0000000
--- a/src/components/MetaItems/MetaItem/MetaItem.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { ReactElement } from 'react';
-import styles from './MetaItem.module.scss';
-
-const MetaItem = ({
- title,
- value,
- values,
- info,
- kind = 'list',
-}: {
- title: string;
- value?: ReactElement | string;
- values?: ReactElement[] | string[];
- info?: string;
- kind: MetaKind;
-}) => {
- return (
- <div className={styles[`wrapper--${kind}`]}>
- <dt className={styles[`title--${kind}`]}>{title}</dt>
- {value && (
- <dd className={styles[`body--${kind}`]} title={info}>
- {value}
- </dd>
- )}
- {values &&
- values.map((currentValue, index) => (
- <dd key={index} className={styles[`body--${kind}`]} title={info}>
- {currentValue}
- </dd>
- ))}
- </div>
- );
-};
-
-export default MetaItem;
diff --git a/src/components/MetaItems/PostsCount/PostsCount.tsx b/src/components/MetaItems/PostsCount/PostsCount.tsx
deleted file mode 100644
index 679abcd..0000000
--- a/src/components/MetaItems/PostsCount/PostsCount.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const PostsCount = ({ total, kind }: { total: number; kind: MetaKind }) => {
- const intl = useIntl();
-
- return (
- <MetaItem
- title={intl.formatMessage({
- defaultMessage: 'Total:',
- description: 'PostCount: total found articles meta label',
- id: 'p1zZ/Z',
- })}
- value={intl.formatMessage(
- {
- defaultMessage:
- '{total, plural, =0 {No articles} one {# article} other {# articles}}',
- description: 'PostCount: total found articles',
- id: '4EMSLO',
- },
- { total }
- )}
- kind={kind}
- />
- );
-};
-
-export default PostsCount;
diff --git a/src/components/MetaItems/ReadingTime/ReadingTime.tsx b/src/components/MetaItems/ReadingTime/ReadingTime.tsx
deleted file mode 100644
index 79d6f3c..0000000
--- a/src/components/MetaItems/ReadingTime/ReadingTime.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const ReadingTime = ({
- time,
- words,
- kind,
-}: {
- time: number;
- words: number;
- kind: MetaKind;
-}) => {
- const intl = useIntl();
- const { locale } = useRouter();
-
- const getEstimation = () => {
- if (time < 0) {
- return intl.formatMessage({
- defaultMessage: 'less than 1 minute',
- description: 'ReadingTime: Reading time value',
- id: 'ySsWZl',
- });
- }
-
- return intl.formatMessage(
- {
- defaultMessage:
- '{time, plural, =0 {# minutes} one {# minute} other {# minutes}}',
- description: 'ReadingTime: reading time value',
- id: 'wdqOpf',
- },
- { time }
- );
- };
-
- return (
- <MetaItem
- title={intl.formatMessage({
- defaultMessage: 'Reading time:',
- description: 'ReadingTime: reading time meta label',
- id: 'n0Gbod',
- })}
- value={getEstimation()}
- info={intl.formatMessage(
- {
- defaultMessage: 'Approximately {number} words',
- description: 'ReadingTime: number of words',
- id: 'k7/SkN',
- },
- { number: words.toLocaleString(locale) }
- )}
- kind={kind}
- />
- );
-};
-
-export default ReadingTime;
diff --git a/src/components/MetaItems/Thematics/Thematics.tsx b/src/components/MetaItems/Thematics/Thematics.tsx
deleted file mode 100644
index e655c5d..0000000
--- a/src/components/MetaItems/Thematics/Thematics.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { ThematicPreview } from '@ts/types/taxonomies';
-import Link from 'next/link';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const Thematics = ({
- list,
- kind,
-}: {
- list: ThematicPreview[];
- kind: MetaKind;
-}) => {
- const intl = useIntl();
-
- const getThematics = () => {
- return list.map((thematic) => {
- return (
- <Link key={thematic.databaseId} href={`/thematique/${thematic.slug}`}>
- <a>{thematic.title}</a>
- </Link>
- );
- });
- };
-
- return (
- <MetaItem
- title={intl.formatMessage(
- {
- defaultMessage:
- '{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}',
- description: 'Thematics: thematics list meta label',
- id: '1r4ujR',
- },
- { thematicsCount: list.length }
- )}
- values={getThematics()}
- kind={kind}
- />
- );
-};
-
-export default Thematics;
diff --git a/src/components/MetaItems/Topics/Topics.tsx b/src/components/MetaItems/Topics/Topics.tsx
deleted file mode 100644
index d5d90f0..0000000
--- a/src/components/MetaItems/Topics/Topics.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { TopicPreview } from '@ts/types/taxonomies';
-import Link from 'next/link';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const Topics = ({ list, kind }: { list: TopicPreview[]; kind: MetaKind }) => {
- const intl = useIntl();
-
- const getTopics = () => {
- return list.map((topic) => {
- return (
- <Link key={topic.databaseId} href={`/sujet/${topic.slug}`}>
- <a>{topic.title}</a>
- </Link>
- );
- });
- };
-
- return (
- <MetaItem
- title={intl.formatMessage(
- {
- defaultMessage:
- '{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}',
- description: 'Topics: topics list meta label',
- id: '0pp/IQ',
- },
- { topicsCount: list.length }
- )}
- values={getTopics()}
- kind={kind}
- />
- );
-};
-
-export default Topics;
diff --git a/src/components/MetaItems/Website/Website.tsx b/src/components/MetaItems/Website/Website.tsx
deleted file mode 100644
index 7d2dc06..0000000
--- a/src/components/MetaItems/Website/Website.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { MetaKind } from '@ts/types/app';
-import { useIntl } from 'react-intl';
-import { MetaItem } from '..';
-
-const Website = ({ url, kind }: { url: string; kind: MetaKind }) => {
- const intl = useIntl();
-
- return (
- <MetaItem
- title={intl.formatMessage({
- defaultMessage: 'Website:',
- description: 'Website: website meta label',
- id: 'JsOoAW',
- })}
- value={<a href={url}>{url}</a>}
- kind={kind}
- />
- );
-};
-
-export default Website;
diff --git a/src/components/MetaItems/index.tsx b/src/components/MetaItems/index.tsx
deleted file mode 100644
index e90d5a6..0000000
--- a/src/components/MetaItems/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import Author from './Author/Author';
-import CommentsCount from './CommentsCount/CommentsCount';
-import Dates from './Dates/Dates';
-import MetaItem from './MetaItem/MetaItem';
-import PostsCount from './PostsCount/PostsCount';
-import ReadingTime from './ReadingTime/ReadingTime';
-import Thematics from './Thematics/Thematics';
-import Topics from './Topics/Topics';
-import Website from './Website/Website';
-
-export {
- Author,
- CommentsCount,
- Dates,
- MetaItem,
- PostsCount,
- ReadingTime,
- Thematics,
- Topics,
- Website,
-};
diff --git a/src/components/Notice/Notice.tsx b/src/components/Notice/Notice.tsx
deleted file mode 100644
index 02b1f12..0000000
--- a/src/components/Notice/Notice.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { NoticeType } from '@ts/types/app';
-import { ReactNode } from 'react';
-import styles from './Notice.module.scss';
-
-const Notice = ({
- children,
- type,
-}: {
- children: ReactNode;
- type: NoticeType;
-}) => {
- const withModifier = `message--${type}`;
-
- return (
- <div className={`${styles.message} ${styles[withModifier]}`}>
- {children}
- </div>
- );
-};
-
-export default Notice;
diff --git a/src/components/Pagination/Pagination.module.scss b/src/components/Pagination/Pagination.module.scss
deleted file mode 100644
index 4d74d1b..0000000
--- a/src/components/Pagination/Pagination.module.scss
+++ /dev/null
@@ -1,92 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-@use "@styles/abstracts/placeholders";
-
-.list {
- @extend %flex-list;
- justify-content: center;
-
- row-gap: var(--spacing-sm);
-}
-
-.link {
- display: block;
- padding: var(--spacing-xs) var(--spacing-sm);
- background: var(--color-bg);
- border: fun.convert-px(2) solid var(--color-primary);
- box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0
- var(--color-primary-darker);
- font-weight: 600;
- text-decoration: none;
-
- @include mix.pointer("fine") {
- padding: var(--spacing-2xs) var(--spacing-xs);
- }
-
- &--current {
- padding: calc(var(--spacing-xs) / 1.5) var(--spacing-sm);
- border-color: var(--color-primary-darker);
- box-shadow: none;
- color: var(--color-primary-darker);
- transform: translateY(#{fun.convert-px(10)});
-
- @include mix.pointer("fine") {
- padding: calc(var(--spacing-2xs) / 1.5) var(--spacing-xs);
- transform: translateY(#{fun.convert-px(7)});
- }
- }
-
- &:not(.link--current) {
- &:hover,
- &:focus {
- border-color: var(--color-primary-light);
- box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0
- var(--color-primary-darker),
- 0 fun.convert-px(2) fun.convert-px(2) fun.convert-px(1)
- var(--color-shadow-dark),
- 0 fun.convert-px(7) fun.convert-px(7) fun.convert-px(2)
- var(--color-shadow-light);
- color: var(--color-primary-light);
- transform: translateY(#{fun.convert-px(-5)});
- }
-
- &:active {
- padding: calc(var(--spacing-xs) / 1.5) var(--spacing-sm);
- border-color: var(--color-primary-dark);
- box-shadow: none;
- color: var(--color-primary-dark);
- transform: translateY(#{fun.convert-px(10)});
-
- @include mix.pointer("fine") {
- padding: calc(var(--spacing-2xs) / 1.5) var(--spacing-xs);
- transform: translateY(#{fun.convert-px(7)});
- }
- }
- }
-}
-
-.item {
- position: relative;
-
- &:first-child {
- .link {
- border-top-left-radius: fun.convert-px(4);
- border-bottom-left-radius: fun.convert-px(4);
- }
- }
-
- &:last-child {
- .link {
- border-top-right-radius: fun.convert-px(4);
- border-bottom-right-radius: fun.convert-px(4);
- }
- }
-
- &:not(:first-child) {
- margin-left: fun.convert-px(-1);
- }
-
- &:not(:last-child) {
- margin-right: fun.convert-px(-1);
- }
-}
diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx
deleted file mode 100644
index 55c366a..0000000
--- a/src/components/Pagination/Pagination.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { settings } from '@utils/config';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import styles from './Pagination.module.scss';
-
-const Pagination = ({ baseUrl, total }: { baseUrl: string; total: number }) => {
- const intl = useIntl();
- const { asPath } = useRouter();
- const totalPages = Math.floor(total / settings.postsPerPage);
- const currentPage = asPath.includes('/page/')
- ? Number(asPath.split(`${baseUrl}/page/`)[1])
- : 1;
- const hasPreviousPage = currentPage !== 1;
- const hasNextPage = currentPage !== totalPages;
-
- const getPreviousPageItem = () => {
- return (
- <li className={styles.item}>
- <Link href={`${baseUrl}/page/${currentPage - 1}`}>
- <a className={styles.link}>
- {intl.formatMessage(
- {
- defaultMessage: '{icon} Previous page',
- description: 'Pagination: previous page link',
- id: 'aMFqPH',
- },
- { icon: '←' }
- )}
- </a>
- </Link>
- </li>
- );
- };
-
- const getNextPageItem = () => {
- return (
- <li className={styles.item}>
- <Link href={`${baseUrl}/page/${currentPage + 1}`}>
- <a className={styles.link}>
- {intl.formatMessage(
- {
- defaultMessage: 'Next page {icon}',
- description: 'Pagination: Next page link',
- id: 'R4yaW6',
- },
- { icon: '→' }
- )}
- </a>
- </Link>
- </li>
- );
- };
-
- const getPages = () => {
- const pages = [];
- for (let i = 1; i <= totalPages; i++) {
- if (i === currentPage) {
- pages.push({
- id: `page-${i}`,
- link: (
- <span className={`${styles.link} ${styles['link--current']}`}>
- {intl.formatMessage(
- {
- defaultMessage: '<a11y>Page </a11y>{number}',
- description: 'Pagination: page number',
- id: 'TSXPzr',
- },
- {
- number: i,
- a11y: (chunks: string) => (
- <span className="screen-reader-text">{chunks}</span>
- ),
- }
- )}
- </span>
- ),
- });
- } else {
- pages.push({
- id: `page-${i}`,
- link: (
- <Link href={`${baseUrl}/page/${i}`}>
- <a className={styles.link}>
- {intl.formatMessage(
- {
- defaultMessage: '<a11y>Page </a11y>{number}',
- description: 'Pagination: page number',
- id: 'TSXPzr',
- },
- {
- number: i,
- a11y: (chunks: string) => (
- <span className="screen-reader-text">{chunks}</span>
- ),
- }
- )}
- </a>
- </Link>
- ),
- });
- }
- }
-
- return pages;
- };
-
- const getItems = () => {
- const pages = getPages();
-
- return pages.map((page) => (
- <li key={page.id} className={styles.item}>
- {page.link}
- </li>
- ));
- };
-
- return (
- <nav className={styles.wrapper} aria-labelledby="pagination-title">
- <h2 id="pagination-title" className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'Pagination',
- description: 'Pagination: pagination title',
- id: 'BAkq7J',
- })}
- </h2>
- <ul className={styles.list}>
- {hasPreviousPage && getPreviousPageItem()}
- {getItems()}
- {hasNextPage && getNextPageItem()}
- </ul>
- </nav>
- );
-};
-
-export default Pagination;
diff --git a/src/components/PaginationCursor/PaginationCursor.tsx b/src/components/PaginationCursor/PaginationCursor.tsx
deleted file mode 100644
index d64f961..0000000
--- a/src/components/PaginationCursor/PaginationCursor.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { useIntl } from 'react-intl';
-import styles from './PaginationCursor.module.scss';
-
-const PaginationCursor = ({
- current,
- total,
-}: {
- current: number;
- total: number;
-}) => {
- const intl = useIntl();
-
- return (
- <div className={styles.wrapper}>
- <div className={styles.info}>
- {intl.formatMessage(
- {
- defaultMessage:
- '{articlesCount, plural, =0 {# loaded articles} one {# loaded article} other {# loaded articles}} out of a total of {total}',
- description: 'PaginationCursor: loaded articles count message',
- id: 'du4MLN',
- },
- { articlesCount: current, total }
- )}
- </div>
- <progress
- className={styles.bar}
- max={total}
- value={current}
- aria-valuemin={0}
- aria-valuemax={total}
- aria-label={intl.formatMessage({
- defaultMessage:
- 'Number of articles loaded out of the total available.',
- description: 'PaginationCursor: loaded articles count aria-label',
- id: 'mC21ht',
- })}
- ></progress>
- </div>
- );
-};
-
-export default PaginationCursor;
diff --git a/src/components/PostFooter/PostFooter.module.scss b/src/components/PostFooter/PostFooter.module.scss
deleted file mode 100644
index 7c1f1ce..0000000
--- a/src/components/PostFooter/PostFooter.module.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/placeholders";
-
-.meta {
- flex-flow: column wrap;
-}
-
-.list {
- @extend %flex-list;
-
- gap: var(--spacing-xs);
-}
-
-.item {
- > a {
- padding: calc(var(--spacing-2xs) / 2) var(--spacing-xs);
- }
-}
diff --git a/src/components/PostFooter/PostFooter.tsx b/src/components/PostFooter/PostFooter.tsx
deleted file mode 100644
index 9bc4053..0000000
--- a/src/components/PostFooter/PostFooter.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { ButtonLink } from '@components/Buttons';
-import { TopicPreview } from '@ts/types/taxonomies';
-import Image from 'next/image';
-import { useIntl } from 'react-intl';
-import styles from './PostFooter.module.scss';
-
-const PostFooter = ({ topics }: { topics: TopicPreview[] }) => {
- const intl = useIntl();
-
- const getTopics = () => {
- return topics.map((topic) => {
- return (
- <li className={styles.item} key={topic.id}>
- <ButtonLink target={`/sujet/${topic.slug}`}>
- {topic.featuredImage && (
- <Image
- src={topic.featuredImage.sourceUrl}
- alt={topic.featuredImage.altText}
- layout="intrinsic"
- width="20"
- height="20"
- />
- )}
- {topic.title}
- </ButtonLink>
- </li>
- );
- });
- };
-
- return (
- <footer>
- {topics.length > 0 && (
- <>
- <dl className={styles.meta}>
- <dt>
- {intl.formatMessage({
- defaultMessage: 'Read more articles about:',
- description: 'PostFooter: read more posts about given subjects',
- id: 'YEudoh',
- })}
- </dt>
- <dd>
- <ul className={styles.list}>{getTopics()}</ul>
- </dd>
- </dl>
- </>
- )}
- </footer>
- );
-};
-
-export default PostFooter;
diff --git a/src/components/PostHeader/PostHeader.tsx b/src/components/PostHeader/PostHeader.tsx
deleted file mode 100644
index c0a6b68..0000000
--- a/src/components/PostHeader/PostHeader.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import PostMeta from '@components/PostMeta/PostMeta';
-import { ArticleMeta } from '@ts/types/articles';
-import { Cover } from '@ts/types/cover';
-import Image from 'next/image';
-import React, { ReactElement } from 'react';
-import styles from './PostHeader.module.scss';
-
-const PostHeader = ({
- cover,
- intro,
- title,
- meta,
-}: {
- cover?: Cover;
- intro?: string | ReactElement;
- meta?: ArticleMeta;
- title: string;
-}) => {
- const getIntro = () => {
- if (React.isValidElement(intro)) {
- const Intro = () => intro;
- return (
- <div className={styles.intro}>
- <Intro />
- </div>
- );
- }
-
- return (
- intro && (
- <div
- className={styles.intro}
- dangerouslySetInnerHTML={{ __html: intro }}
- ></div>
- )
- );
- };
-
- return (
- <header className={styles.wrapper}>
- <div className={styles.body}>
- <h1 className={styles.title}>
- {cover && (
- <span className={styles.cover}>
- <Image src={cover.sourceUrl} alt={cover.altText} layout="fill" />
- </span>
- )}
- {title}
- </h1>
- {meta && <PostMeta kind="article" meta={meta} />}
- {getIntro()}
- </div>
- </header>
- );
-};
-
-export default PostHeader;
diff --git a/src/components/PostMeta/PostMeta.module.scss b/src/components/PostMeta/PostMeta.module.scss
deleted file mode 100644
index d438635..0000000
--- a/src/components/PostMeta/PostMeta.module.scss
+++ /dev/null
@@ -1,31 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.wrapper {
- &--list {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- margin-top: var(--spacing-md);
- font-size: var(--font-size-sm);
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- display: flex;
- flex-flow: column nowrap;
- margin: 0;
- composes: meta from "@components/PostPreview/PostPreview.module.scss";
- }
- }
- }
-
- &--article {
- flex-flow: column wrap;
- margin: var(--spacing-sm) 0 0;
-
- @include mix.media("screen") {
- @include mix.dimensions("xs") {
- font-size: var(--font-size-sm);
- }
- }
- }
-}
diff --git a/src/components/PostMeta/PostMeta.tsx b/src/components/PostMeta/PostMeta.tsx
deleted file mode 100644
index 7fba0be..0000000
--- a/src/components/PostMeta/PostMeta.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import {
- Author,
- CommentsCount,
- Dates,
- PostsCount,
- ReadingTime,
- Thematics,
- Topics,
- Website,
-} from '@components/MetaItems';
-import { MetaKind } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { useRouter } from 'next/router';
-import styles from './PostMeta.module.scss';
-
-const PostMeta = ({
- meta,
- kind = 'list',
-}: {
- meta: ArticleMeta;
- kind?: MetaKind;
-}) => {
- const {
- author,
- commentCount,
- dates,
- readingTime,
- results,
- thematics,
- topics,
- website,
- wordsCount,
- } = meta;
- const { asPath } = useRouter();
- const isThematic = () => asPath.includes('/thematique/');
-
- const wrapperClass = styles[`wrapper--${kind}`];
-
- return (
- <dl className={wrapperClass}>
- {author && <Author name={author.name} kind={kind} />}
- {dates && (
- <Dates
- publication={dates.publication}
- update={dates.update}
- kind={kind}
- />
- )}
- {readingTime !== undefined && wordsCount !== undefined && (
- <ReadingTime time={readingTime} words={wordsCount} kind={kind} />
- )}
- {results !== undefined && <PostsCount total={results} kind={kind} />}
- {!isThematic() && thematics && thematics.length > 0 && (
- <Thematics list={thematics} kind={kind} />
- )}
- {isThematic() && topics && topics.length > 0 && (
- <Topics list={topics} kind={kind} />
- )}
- {website && <Website url={website} kind={kind} />}
- {commentCount !== undefined && (
- <CommentsCount total={commentCount} kind={kind} />
- )}
- </dl>
- );
-};
-
-export default PostMeta;
diff --git a/src/components/PostPreview/PostPreview.module.scss b/src/components/PostPreview/PostPreview.module.scss
deleted file mode 100644
index c30ab75..0000000
--- a/src/components/PostPreview/PostPreview.module.scss
+++ /dev/null
@@ -1,105 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.wrapper {
- --icon-size: #{fun.convert-px(20)};
-
- padding: var(--spacing-2xs) 0 var(--spacing-lg);
- transition: all 0.3s ease-in-out 0s, border 0s;
-
- &:hover {
- --icon-size: #{fun.convert-px(25)};
-
- a {
- > svg {
- :global {
- animation: pulse 1.5s ease-in-out 0.5s infinite;
- }
- }
-
- &:hover {
- > svg {
- animation: none;
- }
- }
- }
- }
-
- &:active {
- --icon-size: 0;
- }
-}
-
-.cover {
- width: auto;
- height: fun.convert-px(100);
- margin: 0 auto var(--spacing-sm);
- position: relative;
- border: fun.convert-px(1) solid var(--color-border);
-}
-
-h2.title {
- background: none;
- text-shadow: none;
-}
-
-@include mix.media("screen") {
- @include mix.dimensions("xs") {
- .wrapper {
- margin: 0;
- padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-md);
- border: fun.convert-px(1) solid var(--color-primary-dark);
- border-radius: fun.convert-px(3);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
- var(--color-shadow),
- fun.convert-px(3) fun.convert-px(3) fun.convert-px(3) fun.convert-px(-1)
- var(--color-shadow-light),
- fun.convert-px(5) fun.convert-px(5) fun.convert-px(7) fun.convert-px(-1)
- var(--color-shadow-light);
- }
-
- .read-more {
- font-size: var(--font-size-sm);
- }
- }
-
- @include mix.dimensions("sm") {
- .wrapper {
- display: grid;
- grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
- grid-template-rows: repeat(3, max-content);
- column-gap: var(--spacing-md);
- }
-
- .cover {
- grid-column: 2;
- grid-row: 1;
- margin: 0 0 var(--spacing-sm);
- }
-
- .header {
- grid-column: 1;
- grid-row: 1;
- align-self: center;
- }
-
- .meta {
- grid-column: 2;
- grid-row: 2 / 4;
- }
-
- .body {
- grid-column: 1;
- grid-row: 2;
- }
-
- .footer {
- grid-column: 1;
- grid-row: 3;
- }
-
- .read-more {
- margin: 0;
- }
- }
-}
diff --git a/src/components/PostPreview/PostPreview.tsx b/src/components/PostPreview/PostPreview.tsx
deleted file mode 100644
index 0b9e332..0000000
--- a/src/components/PostPreview/PostPreview.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { ButtonLink } from '@components/Buttons';
-import { ArrowIcon } from '@components/Icons';
-import PostMeta from '@components/PostMeta/PostMeta';
-import { TitleLevel } from '@ts/types/app';
-import { ArticleMeta, ArticlePreview } from '@ts/types/articles';
-import { settings } from '@utils/config';
-import Image from 'next/image';
-import Link from 'next/link';
-import { FormattedMessage } from 'react-intl';
-import { BlogPosting, WithContext } from 'schema-dts';
-import styles from './PostPreview.module.scss';
-import Script from 'next/script';
-
-const PostPreview = ({
- post,
- titleLevel,
-}: {
- post: ArticlePreview;
- titleLevel: TitleLevel;
-}) => {
- const TitleTag = `h${titleLevel}` as keyof JSX.IntrinsicElements;
- const {
- commentCount,
- dates,
- featuredImage,
- info,
- intro,
- slug,
- thematics,
- title,
- topics,
- } = post;
-
- const meta: ArticleMeta = {
- commentCount: commentCount ? commentCount : 0,
- dates: dates,
- readingTime: info.readingTime,
- thematics: thematics,
- topics: topics,
- wordsCount: info.wordsCount,
- };
-
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
-
- const schemaJsonLd: WithContext<BlogPosting> = {
- '@context': 'https://schema.org',
- '@type': 'BlogPosting',
- name: title,
- description: intro,
- articleBody: intro,
- author: { '@id': `${settings.url}/#branding` },
- commentCount: commentCount ? commentCount : 0,
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: title,
- image: featuredImage?.sourceUrl,
- inLanguage: settings.locales.defaultLocale,
- isBasedOn: `${settings.url}/article/${slug}`,
- isPartOf: { '@id': `${settings.url}/blog` },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- thumbnailUrl: featuredImage?.sourceUrl,
- };
-
- return (
- <>
- <Script
- id="schema-post-preview"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <article className={styles.wrapper}>
- {featuredImage && Object.keys(featuredImage).length > 0 && (
- <div className={styles.cover}>
- <Image
- src={featuredImage.sourceUrl}
- alt={featuredImage.altText}
- layout="fill"
- objectFit="contain"
- />
- </div>
- )}
- <header className={styles.header}>
- <TitleTag className={styles.title}>
- <Link href={`/article/${slug}`}>
- <a>{title}</a>
- </Link>
- </TitleTag>
- </header>
- <div
- className={styles.body}
- dangerouslySetInnerHTML={{ __html: intro }}
- ></div>
- <footer className={styles.footer}>
- <ButtonLink target={`/article/${slug}`} position="left">
- <FormattedMessage
- defaultMessage="Read more<a11y> about {title}</a11y>"
- description="PostPreview: read more link"
- id="bkbrN7"
- values={{
- title,
- a11y: (chunks: string) => (
- <span className="screen-reader-text">{chunks}</span>
- ),
- }}
- />
- <ArrowIcon />
- </ButtonLink>
- </footer>
- <PostMeta meta={meta} />
- </article>
- </>
- );
-};
-
-export default PostPreview;
diff --git a/src/components/PostsList/PostsList.module.scss b/src/components/PostsList/PostsList.module.scss
deleted file mode 100644
index b4ffbd9..0000000
--- a/src/components/PostsList/PostsList.module.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-@use "@styles/abstracts/placeholders";
-
-.section {
- --column-3: 0;
- --grid-gap: 0;
-
- composes: grid from "@styles/layout/_grid.scss";
- align-items: first baseline;
-}
-
-.year {
- grid-column: 2;
- margin: var(--spacing-md) 0 0;
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- grid-column: 1;
- justify-self: end;
- position: sticky;
- top: var(--spacing-xs);
- margin-right: var(--spacing-lg);
- }
- }
-}
-
-.list {
- @extend %reset-ordered-list;
-
- grid-column: 2;
- margin: 0 auto var(--spacing-md);
-}
-
-li.item {
- border-bottom: fun.convert-px(1) solid var(--color-border);
-
- &:not(:last-of-type) {
- margin: 0 0 var(--spacing-md) 0;
- }
-
- &:first-of-type {
- margin-top: var(--spacing-sm);
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- margin-top: 0;
- }
- }
- }
-}
diff --git a/src/components/PostsList/PostsList.tsx b/src/components/PostsList/PostsList.tsx
deleted file mode 100644
index f998846..0000000
--- a/src/components/PostsList/PostsList.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import PostPreview from '@components/PostPreview/PostPreview';
-import { PostsList as PostsListData } from '@ts/types/blog';
-import { sortPostsByYear } from '@utils/helpers/sort';
-import { ForwardedRef, forwardRef, Fragment } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './PostsList.module.scss';
-
-const PostsList = (
- {
- data,
- showYears,
- }: {
- data: PostsListData[];
- showYears: boolean;
- },
- ref: ForwardedRef<HTMLSpanElement>
-) => {
- const intl = useIntl();
- const titleLevel = showYears ? 3 : 2;
-
- const getPostsListByYear = () => {
- const posts = sortPostsByYear(data);
- const years = Object.keys(posts).reverse();
-
- const getLastPostId = () => {
- const oldestYear = Object.keys(posts)[0];
- const lastPost = posts[oldestYear][posts[oldestYear].length - 1];
- return lastPost.id;
- };
-
- return years.map((year) => {
- return (
- <section key={year} className={styles.section}>
- {showYears && (
- <h2 className={styles.year}>
- <span className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'Published on',
- description: 'PostsList: published on year label',
- id: 'EvODgw',
- })}{' '}
- </span>
- {year}
- </h2>
- )}
- <ol className={styles.list}>
- {posts[year].map((post) => {
- const isLastPost = post.id === getLastPostId();
- return (
- <Fragment key={post.id}>
- <li className={styles.item}>
- <PostPreview post={post} titleLevel={titleLevel} />
- </li>
- {isLastPost && (
- <li className={styles.item}>
- <span ref={ref} tabIndex={-1} />
- </li>
- )}
- </Fragment>
- );
- })}
- </ol>
- </section>
- );
- });
- };
-
- const getPostsList = () => {
- return data.map((page) => {
- const getLastPostId = () => {
- const lastPost = page.posts[page.posts.length - 1];
- return lastPost.id;
- };
-
- if (page.posts.length === 0) {
- return (
- <p key="no-result">
- {intl.formatMessage({
- defaultMessage: 'No results found.',
- description: 'PostsList: no results',
- id: 'vK7Sxv',
- })}
- </p>
- );
- } else {
- return (
- <Fragment key={page.pageInfo.endCursor}>
- <ol className={styles.list}>
- {page.posts.map((post) => {
- const isLastPost = post.id === getLastPostId();
- return (
- <Fragment key={post.id}>
- <li key={post.id} className={styles.item}>
- <PostPreview post={post} titleLevel={titleLevel} />
- </li>
- {isLastPost && <span ref={ref} tabIndex={-1} />}
- </Fragment>
- );
- })}
- </ol>
- </Fragment>
- );
- }
- });
- };
-
- return <div>{showYears ? getPostsListByYear() : getPostsList()}</div>;
-};
-
-export default forwardRef(PostsList);
diff --git a/src/components/ProjectPreview/ProjectPreview.module.scss b/src/components/ProjectPreview/ProjectPreview.module.scss
deleted file mode 100644
index 3bf56ec..0000000
--- a/src/components/ProjectPreview/ProjectPreview.module.scss
+++ /dev/null
@@ -1,98 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.article {
- display: flex;
- flex-flow: column nowrap;
- height: 100%;
- padding: var(--spacing-md);
- text-align: center;
-}
-
-.cover {
- height: fun.convert-px(150);
- position: relative;
-}
-
-.title {
- flex: 1;
- margin: var(--spacing-xs) 0;
- background: none;
- text-decoration: underline solid transparent 0;
- text-shadow: none;
- transition: all 0.3s linear 0s;
-}
-
-.body {
- margin: 0 0 var(--spacing-xs);
-}
-
-.footer {
- margin-top: auto;
-}
-
-.meta {
- display: block;
-
- &__item {
- display: flex;
- flex-flow: row wrap;
- place-content: center;
- gap: var(--spacing-2xs);
- }
-}
-
-.link {
- display: block;
- height: 100%;
- background: var(--color-bg);
- color: inherit;
- text-decoration: none;
- border: fun.convert-px(3) solid var(--color-primary);
- border-radius: fun.convert-px(5);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow);
- transition: all 0.3s ease-in-out 0s;
-
- &:hover,
- &:focus,
- &:active {
- color: inherit;
- }
-
- &:hover,
- &:focus {
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow-light),
- fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) fun.convert-px(-3)
- var(--color-shadow-light);
- transform: scale(1.05);
- }
-
- &:focus {
- .title {
- text-decoration: underline solid var(--color-primary) 0.3ex;
- }
- }
-
- &:active {
- box-shadow: 0 0 0 0 var(--color-shadow);
- transform: scale(0.95);
-
- .title {
- text-decoration: none;
- }
- }
-}
-
-.techno {
- padding: 0 var(--spacing-2xs);
- border: fun.convert-px(1) solid var(--color-primary-darker);
-}
diff --git a/src/components/ProjectPreview/ProjectPreview.tsx b/src/components/ProjectPreview/ProjectPreview.tsx
deleted file mode 100644
index 1e1ced2..0000000
--- a/src/components/ProjectPreview/ProjectPreview.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Project } from '@ts/types/app';
-import { slugify } from '@utils/helpers/slugify';
-import Image from 'next/image';
-import Link from 'next/link';
-import { useIntl } from 'react-intl';
-import styles from './ProjectPreview.module.scss';
-
-const ProjectPreview = ({ project }: { project: Project }) => {
- const { id, meta, tagline, title } = project;
- const intl = useIntl();
-
- return (
- <Link href={`/projet/${project.slug}`}>
- <a className={styles.link}>
- <article className={styles.article}>
- <header>
- {meta.hasCover && (
- <div className={styles.cover}>
- <Image
- src={`/projects/${id}.jpg`}
- layout="fill"
- objectFit="contain"
- objectPosition="center"
- alt={intl.formatMessage(
- {
- defaultMessage: '{title} picture',
- description: 'ProjectPreview: cover alt text',
- id: '2pykor',
- },
- { title }
- )}
- />
- </div>
- )}
- <h2 className={styles.title}>{title}</h2>
- </header>
- {tagline && (
- <div
- className={styles.body}
- dangerouslySetInnerHTML={{ __html: tagline }}
- ></div>
- )}
- <footer className={styles.footer}>
- <dl className={styles.meta}>
- {meta.technologies && (
- <div className={styles.meta__item}>
- <dt className="screen-reader-text">
- {intl.formatMessage(
- {
- defaultMessage:
- '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}',
- description: 'ProjectPreview: technologies list label',
- id: 'okFrAO',
- },
- { count: meta.technologies.length }
- )}
- </dt>
- {meta.technologies.map((techno) => (
- <dd key={slugify(techno)} className={styles.techno}>
- {techno}
- </dd>
- ))}
- </div>
- )}
- </dl>
- </footer>
- </article>
- </a>
- </Link>
- );
-};
-
-export default ProjectPreview;
diff --git a/src/components/ProjectSummary/ProjectSummary.module.scss b/src/components/ProjectSummary/ProjectSummary.module.scss
deleted file mode 100644
index cf1e77f..0000000
--- a/src/components/ProjectSummary/ProjectSummary.module.scss
+++ /dev/null
@@ -1,73 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.wrapper {
- margin-bottom: var(--spacing-md);
- padding: var(--spacing-sm) var(--spacing-md) var(--spacing-md);
- border: fun.convert-px(1) solid var(--color-border);
-}
-
-.cover {
- height: fun.convert-px(150);
- position: relative;
-}
-
-.info {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(20ch, 1fr));
- align-items: start;
- justify-content: left;
- column-gap: var(--spacing-md);
- margin: var(--spacing-md) 0 0;
-}
-
-.inline-data {
- display: inline-block;
- margin-top: fun.convert-px(3);
-
- &:not(:last-of-type) {
- margin-right: var(--spacing-xs);
- }
-}
-
-.techno {
- padding: 0 var(--spacing-2xs);
- border: fun.convert-px(1) solid var(--color-primary-darker);
-}
-
-.repo {
- display: block;
- width: 3em;
- height: 3em;
- background: none;
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-1)
- var(--color-shadow),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(4) fun.convert-px(-3)
- var(--color-shadow),
- 0 0 0 0 var(--color-shadow);
- transition: all 0.3s linear 0s;
-
- &:hover,
- &:focus {
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
- fun.convert-px(1) fun.convert-px(1) fun.convert-px(2) fun.convert-px(-1)
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(3) fun.convert-px(4) fun.convert-px(-4)
- var(--color-shadow-light),
- fun.convert-px(6) fun.convert-px(6) fun.convert-px(10) fun.convert-px(-3)
- var(--color-shadow);
- transform: scale(1.15);
- }
-
- &:focus {
- outline: var(--color-primary) dashed fun.convert-px(2);
- }
-
- &:active {
- box-shadow: 0 0 0 0 var(--color-shadow);
- outline: none;
- transform: scale(0.9);
- }
-}
diff --git a/src/components/ProjectSummary/ProjectSummary.tsx b/src/components/ProjectSummary/ProjectSummary.tsx
deleted file mode 100644
index 79e783e..0000000
--- a/src/components/ProjectSummary/ProjectSummary.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import GithubIcon from '@assets/images/social-media/github.svg';
-import GitlabIcon from '@assets/images/social-media/gitlab.svg';
-import { ProjectMeta } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { getFormattedDate } from '@utils/helpers/format';
-import { slugify } from '@utils/helpers/slugify';
-import useGithubApi from '@utils/hooks/useGithubApi';
-import Image from 'next/image';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import styles from './ProjectSummary.module.scss';
-
-const ProjectSummary = ({
- id,
- meta,
-}: {
- id: string;
- title: string;
- meta: ProjectMeta;
-}) => {
- const { hasCover, license, repos, technologies } = meta;
- const intl = useIntl();
- const router = useRouter();
- const locale = router.locale ? router.locale : settings.locales.defaultLocale;
- const { data } = useGithubApi(repos?.github ? repos.github : '');
-
- return (
- <div className={styles.wrapper}>
- {hasCover && (
- <div className={styles.cover}>
- <Image
- src={`/projects/${id}.jpg`}
- alt={intl.formatMessage({
- defaultMessage: '{title} preview',
- description: 'ProjectSummary: cover alt text',
- id: 'mh7tGg',
- })}
- layout="fill"
- objectFit="contain"
- />
- </div>
- )}
- <dl className={styles.info}>
- {data && (
- <div className={styles.info__item}>
- <dt>
- {intl.formatMessage({
- defaultMessage: 'Created on:',
- description: 'ProjectSummary: creation date label',
- id: 'CWi0go',
- })}
- </dt>
- <dd>
- <time dateTime={data.created_at}>
- {getFormattedDate(data.created_at, locale)}
- </time>
- </dd>
- </div>
- )}
- {data && (
- <div className={styles.info__item}>
- <dt>
- {intl.formatMessage({
- defaultMessage: 'Last updated on:',
- description: 'ProjectSummary: update date label',
- id: 'vJ+QDV',
- })}
- </dt>
- <dd>
- <time dateTime={data.updated_at}>
- {getFormattedDate(data.updated_at, locale)}
- </time>
- </dd>
- </div>
- )}
- <div className={styles.info__item}>
- <dt>
- {intl.formatMessage({
- defaultMessage: 'License:',
- description: 'ProjectSummary: license label',
- id: 'hKagVG',
- })}
- </dt>
- <dd>{license}</dd>
- </div>
- {technologies && (
- <div className={styles.info__item}>
- <dt>
- {intl.formatMessage(
- {
- defaultMessage:
- '{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}',
- description: 'ProjectSummary: technologies list label',
- id: 'enwhNm',
- },
- { count: technologies.length }
- )}
- </dt>
- {technologies.map((techno) => (
- <dd
- key={slugify(techno)}
- className={`${styles.techno} ${styles['inline-data']}`}
- >
- {techno}
- </dd>
- ))}
- </div>
- )}
- {repos && (
- <div className={styles.info__item}>
- <dt>
- {intl.formatMessage(
- {
- defaultMessage:
- '{count, plural, =0 {Repositories:} one {Repository:} other {Repositories:}}',
- description: 'ProjectSummary: repositories list label',
- id: 'OTTv+m',
- },
- { count: Object.keys(repos).length }
- )}
- </dt>
- {repos.github && (
- <dd className={styles['inline-data']}>
- <a
- href={`https://github.com/${repos.github}`}
- className={styles.repo}
- >
- <GithubIcon />
- <span className="screen-reader-text">Github</span>
- </a>
- </dd>
- )}
- {repos.gitlab && (
- <dd className={styles['inline-data']}>
- <a
- href={`https://gitlab.com/${repos.gitlab}`}
- className={styles.repo}
- >
- <GitlabIcon />
- <span className="screen-reader-text">Gitlab</span>
- </a>
- </dd>
- )}
- </div>
- )}
- {data && repos && (
- <div>
- <dt>
- {intl.formatMessage({
- defaultMessage: 'Popularity:',
- description: 'ProjectSummary: popularity label',
- id: 'vgMk0q',
- })}
- </dt>
- {repos.github && (
- <dd>
- ⭐&nbsp;
- <a href={`https://github.com/${repos.github}/stargazers`}>
- {intl.formatMessage(
- {
- defaultMessage:
- '{starsCount, plural, =0 {0 stars on Github} one {# star on Github} other {# stars on Github}}',
- description: 'ProjectSummary: technologies list label',
- id: 'aA3hOT',
- },
- { starsCount: data.stargazers_count }
- )}
- </a>
- </dd>
- )}
- </div>
- )}
- </dl>
- </div>
- );
-};
-
-export default ProjectSummary;
diff --git a/src/components/ProjectsList/ProjectsList.module.scss b/src/components/ProjectsList/ProjectsList.module.scss
deleted file mode 100644
index fbed08d..0000000
--- a/src/components/ProjectsList/ProjectsList.module.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/placeholders";
-
-.list {
- --items: 4;
- --items-size: 31ch;
-
- @extend %reset-list;
-
- display: grid;
- grid-template-columns: repeat(
- auto-fit,
- min(calc(100vw - (var(--spacing-md) * 2)), var(--items-size))
- );
- gap: var(--spacing-sm);
- place-content: center;
- width: min(
- calc(100vw - (var(--spacing-md) * 2)),
- calc(
- (var(--items-size) * var(--items)) +
- (var(--spacing-sm) * (var(--items) - 1))
- )
- );
- margin: var(--spacing-sm) auto 0;
-}
diff --git a/src/components/ProjectsList/ProjectsList.tsx b/src/components/ProjectsList/ProjectsList.tsx
deleted file mode 100644
index 07e6a71..0000000
--- a/src/components/ProjectsList/ProjectsList.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import ProjectPreview from '@components/ProjectPreview/ProjectPreview';
-import { Project } from '@ts/types/app';
-import styles from './ProjectsList.module.scss';
-
-const ProjectsList = ({ projects }: { projects: Project[] }) => {
- const getProjectItems = () => {
- return projects.map((project) => {
- return project.title ? (
- <li className={styles.item} key={project.id}>
- <ProjectPreview project={project} />
- </li>
- ) : (
- ''
- );
- });
- };
-
- return <ul className={styles.list}>{getProjectItems()}</ul>;
-};
-
-export default ProjectsList;
diff --git a/src/components/SearchForm/SearchForm.module.scss b/src/components/SearchForm/SearchForm.module.scss
deleted file mode 100644
index 4debfbb..0000000
--- a/src/components/SearchForm/SearchForm.module.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.title {
- margin-bottom: var(--spacing-sm);
- color: var(--color-primary-dark);
- font-size: var(--font-size-lg);
- font-weight: 600;
-}
diff --git a/src/components/SearchForm/SearchForm.tsx b/src/components/SearchForm/SearchForm.tsx
deleted file mode 100644
index f4735af..0000000
--- a/src/components/SearchForm/SearchForm.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { ButtonSubmit } from '@components/Buttons';
-import { Field, Form } from '@components/FormElements';
-import { SearchIcon } from '@components/Icons';
-import { useRouter } from 'next/router';
-import { FormEvent, useEffect, useRef, useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './SearchForm.module.scss';
-
-const SearchForm = ({ isOpened }: { isOpened: boolean }) => {
- const intl = useIntl();
- const [query, setQuery] = useState('');
- const inputRef = useRef<HTMLInputElement>(null);
- const router = useRouter();
-
- useEffect(() => {
- setTimeout(() => {
- if (isOpened && inputRef.current) {
- inputRef.current.focus();
- }
- }, 400);
- }, [isOpened]);
-
- const launchSearch = (e: FormEvent) => {
- e.preventDefault();
- router.push({ pathname: '/recherche', query: { s: query } });
- setQuery('');
- };
-
- return (
- <>
- <div className={styles.title}>
- {intl.formatMessage({
- defaultMessage: 'Search',
- description: 'SearchForm : form title',
- id: 'eFMu2E',
- })}
- </div>
- <Form submitHandler={launchSearch} kind="search" id="search">
- <label htmlFor="search-query" className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'Keywords:',
- description: 'SearchForm: search field label',
- id: 'YvMPuD',
- })}
- </label>
- <Field
- ref={inputRef}
- id="search-query"
- name="search-query"
- kind="search"
- value={query}
- setValue={setQuery}
- required={true}
- />
- <ButtonSubmit modifier="search">
- <SearchIcon />
- <span className="screen-reader-text">
- {intl.formatMessage({
- defaultMessage: 'Search',
- description: 'SearchForm: search button text',
- id: 'AnaPbu',
- })}
- </span>
- </ButtonSubmit>
- </Form>
- </>
- );
-};
-
-export default SearchForm;
diff --git a/src/components/Settings/AckeeSelect/AckeeSelect.tsx b/src/components/Settings/AckeeSelect/AckeeSelect.tsx
deleted file mode 100644
index f711fe2..0000000
--- a/src/components/Settings/AckeeSelect/AckeeSelect.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { Field, Label } from '@components/FormElements';
-import Tooltip from '@components/Tooltip/Tooltip';
-import { LocalStorage } from '@services/local-storage';
-import { useAckeeTracker } from '@utils/providers/ackee';
-import { useEffect, useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './AckeeSelect.module.scss';
-
-const AckeeSelect = () => {
- const intl = useIntl();
- const options = [
- {
- id: 'partial',
- name: intl.formatMessage({
- defaultMessage: 'Partial',
- description: 'AckeeSelect: partial option name',
- id: 'e/8Kyj',
- }),
- value: 'partial',
- },
- {
- id: 'full',
- name: intl.formatMessage({
- defaultMessage: 'Full',
- description: 'AckeeSelect: full option name',
- id: 'PzRpPw',
- }),
- value: 'full',
- },
- ];
- const [value, setValue] = useState<string>('full');
- const { setDetailed } = useAckeeTracker();
-
- useEffect(() => {
- setDetailed(value === 'full');
- }, [setDetailed, value]);
-
- useEffect(() => {
- const initialState = LocalStorage.get('ackee-tracking');
- if (initialState) setValue(initialState);
- }, []);
-
- useEffect(() => {
- LocalStorage.set('ackee-tracking', `${value}`);
- }, [value]);
-
- const label = (
- <Label
- body={intl.formatMessage({
- defaultMessage: 'Tracking:',
- description: 'AckeeSelect: select label',
- id: '2pmylc',
- })}
- htmlFor="ackee-settings"
- kind="settings"
- />
- );
-
- const message = [
- intl.formatMessage({
- defaultMessage: 'Partial includes only page url, views and duration.',
- description: 'AckeeSelect: tooltip message',
- id: 'skb4W5',
- }),
- intl.formatMessage({
- defaultMessage:
- 'Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.',
- description: 'AckeeSelect: tooltip message',
- id: 'Ogccx6',
- }),
- ];
-
- return (
- <div className={styles.wrapper}>
- <Field
- id="ackee-settings"
- name="ackee-settings"
- kind="select"
- label={label}
- options={options}
- value={value}
- setValue={setValue}
- />
- <Tooltip
- message={message}
- title={intl.formatMessage({
- defaultMessage: 'Ackee tracking (analytics)',
- description: 'AckeeSelect: tooltip title',
- id: 'F1EQX3',
- })}
- />
- </div>
- );
-};
-
-export default AckeeSelect;
diff --git a/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx b/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx
deleted file mode 100644
index 20ad267..0000000
--- a/src/components/Settings/PrismThemeToggle/PrismThemeToggle.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { Toggle } from '@components/FormElements';
-import { MoonIcon, SunIcon } from '@components/Icons';
-import Spinner from '@components/Spinner/Spinner';
-import { usePrismTheme } from '@utils/providers/prism-theme';
-import { useEffect, useState } from 'react';
-import { useIntl } from 'react-intl';
-
-const PrismThemeToggle = () => {
- const intl = useIntl();
- const [isMounted, setIsMounted] = useState<boolean>(false);
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- const { theme, setTheme, resolvedTheme } = usePrismTheme();
- const [isDarkTheme, setIsDarkTheme] = useState<boolean>(theme === 'dark');
-
- useEffect(() => {
- if (theme === 'system') {
- setIsDarkTheme(resolvedTheme === 'dark');
- } else {
- setIsDarkTheme(theme === 'dark');
- }
- }, [theme, resolvedTheme]);
-
- const updateTheme = () => {
- isDarkTheme ? setTheme('light') : setTheme('dark');
- setIsDarkTheme(!isDarkTheme);
- };
-
- if (!isMounted) return <Spinner />;
-
- return (
- <Toggle
- id="prism-theme"
- label={intl.formatMessage({
- defaultMessage: 'Code blocks:',
- description: 'PrismThemeToggle: toggle label',
- id: 'w0UfY0',
- })}
- leftChoice={<SunIcon />}
- rightChoice={<MoonIcon />}
- value={isDarkTheme}
- changeHandler={updateTheme}
- />
- );
-};
-
-export default PrismThemeToggle;
diff --git a/src/components/Settings/ReduceMotion/ReduceMotion.tsx b/src/components/Settings/ReduceMotion/ReduceMotion.tsx
deleted file mode 100644
index 00562cd..0000000
--- a/src/components/Settings/ReduceMotion/ReduceMotion.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Toggle } from '@components/FormElements';
-import { LocalStorage } from '@services/local-storage';
-import { useEffect, useState } from 'react';
-import { useIntl } from 'react-intl';
-
-const ReduceMotion = () => {
- const intl = useIntl();
- const [isDeactivated, setIsDeactivated] = useState<boolean>(false);
-
- useEffect(() => {
- const initialState = LocalStorage.get('reduced-motion');
- if (initialState) setIsDeactivated(initialState === 'true' ? true : false);
- }, []);
-
- useEffect(() => {
- document.documentElement.dataset.reducedMotion = `${isDeactivated}`;
- LocalStorage.set('reduced-motion', `${isDeactivated}`);
- }, [isDeactivated]);
-
- const updateState = () => {
- setIsDeactivated(!isDeactivated);
- };
-
- return (
- <Toggle
- id="reduced-motion"
- label={intl.formatMessage({
- defaultMessage: 'Animations:',
- description: 'ReduceMotion: toggle label',
- id: 'X3PDXO',
- })}
- leftChoice={intl.formatMessage({
- defaultMessage: 'On',
- description: 'ReduceMotion: toggle on label',
- id: 'qPU/Qn',
- })}
- rightChoice={intl.formatMessage({
- defaultMessage: 'Off',
- description: 'ReduceMotion: toggle off label',
- id: 'w1nIrj',
- })}
- value={isDeactivated}
- changeHandler={updateState}
- />
- );
-};
-
-export default ReduceMotion;
diff --git a/src/components/Settings/Settings.module.scss b/src/components/Settings/Settings.module.scss
deleted file mode 100644
index fe6b17b..0000000
--- a/src/components/Settings/Settings.module.scss
+++ /dev/null
@@ -1,17 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.title {
- --icon-size: #{fun.convert-px(30)};
-
- display: flex;
- flex-flow: row nowrap;
- gap: var(--spacing-2xs);
- margin-bottom: var(--spacing-md);
- color: var(--color-primary-dark);
- font-size: var(--font-size-lg);
- font-weight: 600;
-
- svg {
- margin: 0;
- }
-}
diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx
deleted file mode 100644
index fec4c45..0000000
--- a/src/components/Settings/Settings.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { CogIcon } from '@components/Icons';
-import ThemeToggle from '@components/Settings/ThemeToggle/ThemeToggle';
-import { useIntl } from 'react-intl';
-import AckeeSelect from './AckeeSelect/AckeeSelect';
-import PrismThemeToggle from './PrismThemeToggle/PrismThemeToggle';
-import ReduceMotion from './ReduceMotion/ReduceMotion';
-import styles from './Settings.module.scss';
-
-const Settings = () => {
- const intl = useIntl();
-
- return (
- <>
- <div className={styles.title}>
- <CogIcon />{' '}
- {intl.formatMessage({
- defaultMessage: 'Settings',
- description: 'Settings: modal title',
- id: 'bHEmkY',
- })}
- </div>
- <ThemeToggle />
- <ReduceMotion />
- <PrismThemeToggle />
- <AckeeSelect />
- </>
- );
-};
-
-export default Settings;
diff --git a/src/components/Settings/ThemeToggle/ThemeToggle.tsx b/src/components/Settings/ThemeToggle/ThemeToggle.tsx
deleted file mode 100644
index ec2cee1..0000000
--- a/src/components/Settings/ThemeToggle/ThemeToggle.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Toggle } from '@components/FormElements';
-import { MoonIcon, SunIcon } from '@components/Icons';
-import Spinner from '@components/Spinner/Spinner';
-import { useTheme } from 'next-themes';
-import { useEffect, useState } from 'react';
-import { useIntl } from 'react-intl';
-
-const ThemeToggle = () => {
- const intl = useIntl();
- const [isMounted, setIsMounted] = useState<boolean>(false);
- const { resolvedTheme, setTheme } = useTheme();
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- if (!isMounted) return <Spinner />;
-
- const isDarkTheme = resolvedTheme === 'dark';
-
- const updateTheme = () => {
- setTheme(isDarkTheme ? 'light' : 'dark');
- };
-
- return (
- <Toggle
- id="dark-theme"
- label={intl.formatMessage({
- defaultMessage: 'Theme:',
- description: 'ThemeToggle: toggle label',
- id: 'O9XLDc',
- })}
- leftChoice={<SunIcon />}
- rightChoice={<MoonIcon />}
- value={isDarkTheme}
- changeHandler={updateTheme}
- />
- );
-};
-
-export default ThemeToggle;
diff --git a/src/components/Sidebar/Sidebar.module.scss b/src/components/Sidebar/Sidebar.module.scss
deleted file mode 100644
index fb6230d..0000000
--- a/src/components/Sidebar/Sidebar.module.scss
+++ /dev/null
@@ -1,43 +0,0 @@
-@use "@styles/abstracts/mixins" as mix;
-
-.wrapper {
- grid-column: 2;
-
- &--left {
- margin: var(--spacing-md) 0;
- }
-
- &--right {
- margin: var(--spacing-md) 0 0;
- }
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- align-self: stretch;
- margin: 0 var(--spacing-xs) var(--spacing-md);
-
- &--right {
- grid-row: 2 / 4;
- grid-column: 3;
- }
- }
-
- @include mix.dimensions("lg") {
- &--left {
- grid-row: 2 / 4;
- grid-column: 1;
- }
- }
- }
-}
-
-.body {
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- align-self: flex-start;
- width: 100%;
- position: sticky;
- top: var(--spacing-xs);
- }
- }
-}
diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx
deleted file mode 100644
index 9e2079d..0000000
--- a/src/components/Sidebar/Sidebar.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Children, cloneElement, isValidElement, ReactNode } from 'react';
-import styles from './Sidebar.module.scss';
-
-type SidebarPosition = 'left' | 'right';
-
-const Sidebar = ({
- children,
- position,
- ariaLabel,
- title,
-}: {
- children: ReactNode;
- position: SidebarPosition;
- ariaLabel?: string;
- title?: string;
-}) => {
- const childrenWithProps = Children.map(children, (child) => {
- if (isValidElement(child)) {
- return cloneElement(child, { titleLevel: title ? 3 : 2 });
- }
- return child;
- });
-
- const positionClass = `wrapper--${position}`;
-
- return (
- <aside
- className={`${styles.wrapper} ${styles[positionClass]}`}
- aria-label={ariaLabel}
- aria-labelledby={title ? `${position}-sidebar-title` : undefined}
- >
- <div className={styles.body}>
- {title && <h2 id={`${position}-sidebar-title`}>{title}</h2>}
- {childrenWithProps}
- </div>
- </aside>
- );
-};
-
-export default Sidebar;
diff --git a/src/components/Toolbar/Toolbar.module.scss b/src/components/Toolbar/Toolbar.module.scss
deleted file mode 100644
index debb3b7..0000000
--- a/src/components/Toolbar/Toolbar.module.scss
+++ /dev/null
@@ -1,114 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.wrapper {
- --btn-size: #{fun.convert-px(60)};
-
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: space-around;
- width: 100%;
- height: var(--toolbar-size);
- position: fixed;
- bottom: 0;
- left: 0;
- z-index: 5;
- background: var(--color-bg);
- border-top: fun.convert-px(4) solid;
- border-image: radial-gradient(
- ellipse at top,
- var(--color-primary-lighter) 20%,
- var(--color-primary) 100%
- )
- 1;
- box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1)
- var(--color-shadow-dark);
-
- :global {
- animation: slide-in-from-bottom 0.8s ease-in-out 0s 1;
- }
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- --toolbar-size: auto;
-
- justify-content: flex-end;
- gap: var(--spacing-sm);
- width: auto;
- background: inherit;
- border: none;
- box-shadow: none;
- position: relative;
- left: unset;
- margin-right: unset;
- transform: unset;
-
- :global {
- animation: slide-in-from-top 1s ease-in-out 0s 1;
- }
- }
- }
-}
-
-.menu {
- padding: var(--spacing-md);
- position: absolute;
- bottom: 100%;
- left: 0;
- right: 0;
- background: var(--color-bg-secondary);
- border-top: fun.convert-px(4) solid;
- border-bottom: fun.convert-px(4) solid;
- border-image: radial-gradient(
- ellipse at top,
- var(--color-primary-lighter) 20%,
- var(--color-primary) 100%
- )
- 1;
- box-shadow: fun.convert-px(2) fun.convert-px(-2) fun.convert-px(3)
- fun.convert-px(-1) var(--color-shadow-dark);
- transition: all 0.7s ease-in-out 0s;
-
- &--closed {
- transform: translateX(-100%);
- visibility: hidden;
- }
-
- &--opened {
- transform: translateX(0);
- visibility: visible;
- }
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- width: fun.convert-px(500);
- left: unset;
- right: unset;
- top: 120%;
- bottom: unset;
- border: fun.convert-px(4) solid;
- border-image: radial-gradient(
- ellipse at top,
- var(--color-primary-lighter) 20%,
- var(--color-primary) 100%
- )
- 1;
- box-shadow: fun.convert-px(2) fun.convert-px(2) fun.convert-px(3)
- fun.convert-px(1) var(--color-shadow-dark);
- transform-origin: 50% -200%;
- transition: all 0.8s ease-in-out 0s;
-
- &--closed {
- opacity: 0;
- transform: perspective(20rem) translate3d(0, 100%, -20rem);
- visibility: hidden;
- }
-
- &--opened {
- opacity: 1;
- transform: none;
- }
- }
- }
-}
diff --git a/src/components/Toolbar/Toolbar.tsx b/src/components/Toolbar/Toolbar.tsx
deleted file mode 100644
index 17f9ef9..0000000
--- a/src/components/Toolbar/Toolbar.tsx
+++ /dev/null
@@ -1,162 +0,0 @@
-import { ButtonToolbar } from '@components/Buttons';
-import MainNav from '@components/MainNav/MainNav';
-import Spinner from '@components/Spinner/Spinner';
-import dynamic from 'next/dynamic';
-import { RefObject, useCallback, useEffect, useRef, useState } from 'react';
-import styles from './Toolbar.module.scss';
-
-const DynamicSearchForm = dynamic(
- () => import('@components/SearchForm/SearchForm'),
- {
- loading: () => <Spinner />,
- }
-);
-
-const DynamicSettings = dynamic(() => import('@components/Settings/Settings'), {
- loading: () => <Spinner />,
-});
-
-const Toolbar = () => {
- const [isNavOpened, setIsNavOpened] = useState<boolean>(false);
- const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false);
- const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false);
- const mainNavRef = useRef<HTMLDivElement>(null);
- const searchBtnRef = useRef<HTMLButtonElement>(null);
- const searchModalRef = useRef<HTMLDivElement>(null);
- const settingsBtnRef = useRef<HTMLButtonElement>(null);
- const settingsModalRef = useRef<HTMLDivElement>(null);
-
- useEffect(() => {
- if (isNavOpened) {
- setIsSearchOpened(false);
- setIsSettingsOpened(false);
- }
- }, [isNavOpened]);
-
- useEffect(() => {
- if (isSearchOpened) {
- setIsNavOpened(false);
- setIsSettingsOpened(false);
- }
- }, [isSearchOpened]);
-
- useEffect(() => {
- if (isSettingsOpened) {
- setIsNavOpened(false);
- setIsSearchOpened(false);
- }
- }, [isSettingsOpened]);
-
- const isClickOutside = (
- ref: RefObject<HTMLDivElement>,
- target: EventTarget
- ) => {
- return ref.current && !ref.current.contains(target as Node);
- };
-
- const isToggleBtn = (ref: RefObject<HTMLDivElement>, target: EventTarget) => {
- return (
- ref.current &&
- ref.current.previousElementSibling &&
- ref.current.previousElementSibling.contains(target as Node)
- );
- };
-
- const isSearchBtn = useCallback((target: HTMLElement) => {
- return (
- target === searchBtnRef.current || searchBtnRef.current?.contains(target)
- );
- }, []);
-
- const isSettingsBtn = useCallback((target: HTMLElement) => {
- return (
- target === settingsBtnRef.current ||
- settingsBtnRef.current?.contains(target)
- );
- }, []);
-
- const handleVisibility = useCallback(
- (e: MouseEvent | FocusEvent) => {
- let ref: RefObject<HTMLDivElement> | null = null;
- if (isNavOpened) ref = mainNavRef;
- if (isSearchOpened) ref = searchModalRef;
- if (isSettingsOpened) ref = settingsModalRef;
-
- if (!ref || !ref.current || !ref.current.id) return;
- if (!isClickOutside(ref, e.target as Node)) return;
- if (isToggleBtn(ref, e.target as Node)) return;
-
- if (
- ref.current.id === 'main-nav' &&
- !isSettingsBtn(e.target as HTMLElement) &&
- !isSearchBtn(e.target as HTMLElement)
- ) {
- setIsNavOpened(false);
- }
-
- if (
- ref.current.id === 'search-modal' &&
- !isSettingsBtn(e.target as HTMLElement)
- )
- setIsSearchOpened(false);
- if (
- ref.current.id === 'settings-modal' &&
- !isSearchBtn(e.target as HTMLElement)
- )
- setIsSettingsOpened(false);
- },
- [isNavOpened, isSearchOpened, isSettingsOpened, isSearchBtn, isSettingsBtn]
- );
-
- useEffect(() => {
- document.addEventListener('mousedown', handleVisibility);
- document.addEventListener('focusin', handleVisibility);
-
- return () => {
- document.removeEventListener('mousedown', handleVisibility);
- document.removeEventListener('focusin', handleVisibility);
- };
- }, [handleVisibility]);
-
- const searchClasses = `${styles.menu} ${
- isSearchOpened ? styles['menu--opened'] : styles['menu--closed']
- }`;
-
- const settingsClasses = `${styles.menu} ${
- isSettingsOpened ? styles['menu--opened'] : styles['menu--closed']
- }`;
-
- return (
- <div className={styles.wrapper}>
- <MainNav
- ref={mainNavRef}
- isOpened={isNavOpened}
- setIsOpened={setIsNavOpened}
- />
- <ButtonToolbar
- ref={searchBtnRef}
- type="search"
- isActivated={isSearchOpened}
- setIsActivated={setIsSearchOpened}
- />
- <div id="search-modal" className={searchClasses} ref={searchModalRef}>
- <DynamicSearchForm isOpened={isSearchOpened} />
- </div>
- <ButtonToolbar
- ref={settingsBtnRef}
- type="settings"
- isActivated={isSettingsOpened}
- setIsActivated={setIsSettingsOpened}
- />
- <div
- id="settings-modal"
- className={settingsClasses}
- ref={settingsModalRef}
- >
- <DynamicSettings />
- </div>
- </div>
- );
-};
-
-export default Toolbar;
diff --git a/src/components/Tooltip/Tooltip.module.scss b/src/components/Tooltip/Tooltip.module.scss
deleted file mode 100644
index 34fa23d..0000000
--- a/src/components/Tooltip/Tooltip.module.scss
+++ /dev/null
@@ -1,120 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.title {
- padding: var(--spacing-2xs) var(--spacing-xs);
- position: absolute;
- top: calc(var(--spacing-sm) * -1);
- left: var(--spacing-lg);
- background: var(--color-bg);
- border: fun.convert-px(1) solid var(--color-primary-dark);
- box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow);
- color: var(--color-primary-darker);
- font-size: var(--font-size-sm);
- font-variant: small-caps;
- font-weight: 500;
-
- @include mix.media("screen") {
- @include mix.dimensions(null, "2xs", "height") {
- top: 0;
- }
-
- @include mix.dimensions("md") {
- left: var(--spacing-md);
- }
- }
-
- &::before {
- content: "?";
- padding: var(--spacing-2xs);
- position: absolute;
- top: fun.convert-px(-1);
- bottom: fun.convert-px(-1);
- right: 100%;
- background: var(--color-primary-dark);
- border: fun.convert-px(1) solid var(--color-primary-dark);
- box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow);
- color: var(--color-fg-inverted);
- font-weight: 600;
- }
-}
-
-.message {
- transition: all 0.5s ease-in-out 0;
-}
-
-.wrapper {
- padding: 9% 6% var(--spacing-sm) 6%;
- position: absolute;
- bottom: 30%;
- left: fun.convert-px(15);
- right: fun.convert-px(15);
- background: var(--color-bg);
- border: fun.convert-px(2) solid var(--color-primary-dark);
- border-radius: fun.convert-px(3);
- box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow),
- fun.convert-px(2) fun.convert-px(2) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light);
- transform-origin: bottom;
-
- @include mix.media("screen") {
- @include mix.dimensions(null, "2xs", "height") {
- overflow-y: auto;
- top: 18%;
- }
-
- @include mix.dimensions("sm") {
- bottom: unset;
- left: fun.convert-px(15);
- right: fun.convert-px(15);
- top: 100%;
- transform-origin: top;
- }
- }
-
- ul,
- p {
- margin: 0;
- padding: 0;
- }
-}
-
-.hidden {
- visibility: hidden;
- opacity: 0;
- transition: all 0.5s ease-in-out 0s, opacity 0.3s ease-in-out 0.2s;
- transform: scaleY(0);
-
- .message,
- .title {
- opacity: 0;
- }
-
- .message {
- transition: all 0.3s ease-in-out 0s;
- }
-
- .title {
- transition: all 0.2s ease-in-out 0.2s;
- }
-}
-
-.visible {
- visibility: visible;
- opacity: 1;
- transform: scaleY(1);
- transition: all 0.8s ease-in-out 0s, opacity 0.7s ease-in-out 0.2s;
-
- .message,
- .title {
- opacity: 1;
- }
-
- .message {
- transition: all 0.5s ease-in-out 0.2s;
- }
-
- .title {
- transition: all 0.4s ease-in-out 0s;
- }
-}
diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx
deleted file mode 100644
index 56a87ab..0000000
--- a/src/components/Tooltip/Tooltip.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { ButtonHelp } from '@components/Buttons';
-import { useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './Tooltip.module.scss';
-
-const Tooltip = ({
- message,
- title,
-}: {
- message: string | string[];
- title: string;
-}) => {
- const intl = useIntl();
- const [isOpen, setIsOpen] = useState<boolean>(false);
-
- const getMessageFromArray = (strings: string[]) => {
- let keyIndex = 0;
- return (
- <ul>
- {strings.map((string) => {
- keyIndex = keyIndex + 1;
- return <li key={`message-${keyIndex}`}>{string}</li>;
- })}
- </ul>
- );
- };
-
- const buttonTitle = isOpen
- ? intl.formatMessage({
- defaultMessage: 'Close help',
- description: 'Tooltip: button title',
- id: '9kx83j',
- })
- : intl.formatMessage({
- defaultMessage: 'Show help',
- description: 'Tooltip: button title',
- id: 'A5n+C9',
- });
-
- const wrapperModifier = isOpen ? styles.visible : styles.hidden;
-
- return (
- <div>
- <ButtonHelp
- showHelp={isOpen}
- setShowHelp={setIsOpen}
- title={buttonTitle}
- />
- <div className={`${styles.wrapper} ${wrapperModifier}`}>
- <div className={styles.title}>{title}</div>
- <div className={styles.message}>
- {Array.isArray(message) ? getMessageFromArray(message) : message}
- </div>
- </div>
- </div>
- );
-};
-
-export default Tooltip;
diff --git a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.module.scss b/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.module.scss
deleted file mode 100644
index 6a7757d..0000000
--- a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.module.scss
+++ /dev/null
@@ -1,146 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-.title {
- margin: 0;
- padding: 0;
- background: none;
- font-size: var(--font-size-xl);
- text-align: left;
-}
-
-.icon {
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- justify-content: center;
- width: fun.convert-px(30);
- height: fun.convert-px(30);
- background: var(--color-bg);
- border: fun.convert-px(1) solid var(--color-primary);
- border-radius: fun.convert-px(3);
- color: var(--color-primary);
- font-weight: 800;
- transition: all 0.25s ease-in-out 0s;
-
- &::before,
- &::after {
- content: "";
- background: var(--color-primary);
- transition: all 0.4s ease-out 0s;
- }
-
- &::before {
- width: 10%;
- height: 60%;
- position: relative;
- left: 30%;
- }
-
- &::after {
- width: 60%;
- height: 10%;
- position: relative;
- left: -5%;
- }
-}
-
-.body {
- width: 100%;
- max-height: 0;
- margin: 0 0 fun.convert-px(-6); // collapse borders
- overflow: hidden;
- visibility: hidden;
- transition: all 0.6s cubic-bezier(0, 1, 0, 1) 0s, margin 0.2s ease-in-out 0s,
- border 0.1s ease-in-out 0.3s, visibility 0.1s linear 0.6s;
-
- &--borders {
- border: 0 solid transparent;
- }
-
- > *:last-child {
- margin-bottom: 0;
- }
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- font-size: var(--font-size-sm);
- font-weight: 500;
- }
- }
-}
-
-.wrapper {
- --header-height: #{fun.convert-px(65)};
-
- display: flex;
- flex-flow: column;
-
- &--expanded {
- .icon::before {
- height: 0;
- }
-
- .body {
- max-height: 10000px; // needs a fixed value for transition.
- margin: var(--spacing-sm) 0;
- overflow: visible;
- visibility: visible;
- transition: visibility 0.1s linear 0s, max-height 0.6s linear 0s,
- margin 0.2s ease-in-out 0s;
-
- &--borders {
- border: fun.convert-px(2) solid var(--color-primary-dark);
- }
- }
- }
-}
-
-.wrapper--expanded.wrapper--toc {
- @include mix.media("screen") {
- @include mix.dimensions("lg") {
- max-height: 100vh;
-
- .body {
- overflow-y: auto;
- }
- }
- }
-}
-
-.header {
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- justify-content: space-between;
- gap: var(--spacing-md);
- width: 100%;
- min-height: var(--header-height);
- padding: 0;
- position: sticky;
- top: 0;
- z-index: 3;
- background: var(--color-bg);
- border: none;
- border-top: fun.convert-px(2) solid var(--color-primary-dark);
- border-bottom: fun.convert-px(2) solid var(--color-primary-dark);
- cursor: pointer;
-
- &:hover,
- &:focus {
- .icon {
- background: var(--color-primary-light);
- color: var(--color-fg-inverted);
- transform: scale(1.2);
-
- &::before,
- &::after {
- background: var(--color-bg);
- }
- }
- }
-
- > button {
- padding: 0 var(--spacing-xs);
- }
-}
diff --git a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx b/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx
deleted file mode 100644
index 38e57ad..0000000
--- a/src/components/WidgetParts/ExpandableWidget/ExpandableWidget.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { TitleLevel } from '@ts/types/app';
-import { ReactNode, useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './ExpandableWidget.module.scss';
-
-const ExpandableWidget = ({
- children,
- title,
- titleLevel = 2,
- expand = false,
- withBorders = false,
- kind = 'regular',
-}: {
- children: ReactNode;
- title: string;
- titleLevel?: TitleLevel;
- expand?: boolean;
- withBorders?: boolean;
- kind?: 'regular' | 'toc';
-}) => {
- const intl = useIntl();
- const [isExpanded, setIsExpanded] = useState<boolean>(expand);
-
- const handleExpanse = () => setIsExpanded((prev) => !prev);
-
- const TitleTag = `h${titleLevel}` as keyof JSX.IntrinsicElements;
-
- const wrapperKindClass = styles[`wrapper--${kind}`];
- const wrapperClasses = `${styles.wrapper} ${
- isExpanded ? styles['wrapper--expanded'] : ''
- } ${wrapperKindClass}`;
-
- const bodyClasses = `${styles.body} ${
- withBorders ? styles['body--borders'] : ''
- }`;
-
- return (
- <div className={wrapperClasses}>
- <button type="button" className={styles.header} onClick={handleExpanse}>
- <span className="screen-reader-text">
- {isExpanded
- ? intl.formatMessage({
- defaultMessage: 'Collapse',
- description: 'ExpandableWidget: collapse text',
- id: 'WRkY1/',
- })
- : intl.formatMessage({
- defaultMessage: 'Expand',
- description: 'ExpandableWidget: expand text',
- id: 'hV0qHp',
- })}
- </span>
- <TitleTag className={styles.title}>{title}</TitleTag>
- <span className={styles.icon} aria-hidden={true}></span>
- </button>
- <div className={bodyClasses}>{children}</div>
- </div>
- );
-};
-
-export default ExpandableWidget;
diff --git a/src/components/WidgetParts/List/List.module.scss b/src/components/WidgetParts/List/List.module.scss
deleted file mode 100644
index 958f792..0000000
--- a/src/components/WidgetParts/List/List.module.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-
-.list {
- margin: 0;
- padding: 0;
- list-style-type: none;
-
- .list {
- border: none;
- border-top: fun.convert-px(1) solid var(--color-primary-dark);
- }
-
- li {
- margin: 0;
-
- &:not(:last-of-type) {
- border-bottom: fun.convert-px(1) solid var(--color-primary-dark);
- }
- }
-
- a {
- display: flex;
- flex-flow: row nowrap;
- width: 100%;
- padding: var(--spacing-2xs) var(--spacing-xs);
- background: none;
- text-decoration: underline solid transparent 0;
- transition: all 0.2s ease-in-out 0s, font-weight 0s,
- text-decoration-color 0s;
-
- &:hover,
- &:focus {
- background: var(--color-bg-secondary);
- font-weight: 600;
- }
-
- &:focus {
- color: var(--color-primary);
- text-decoration-color: var(--color-primary-light);
- text-decoration-thickness: 0.25ex;
- }
-
- &:active {
- background: var(--color-bg-tertiary);
- text-decoration-color: transparent;
- text-decoration-thickness: 0;
- }
- }
-}
diff --git a/src/components/WidgetParts/List/List.tsx b/src/components/WidgetParts/List/List.tsx
deleted file mode 100644
index 317c4d1..0000000
--- a/src/components/WidgetParts/List/List.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import styles from './List.module.scss';
-
-const List = ({ items }: { items: Array<any> }) => {
- return <ul className={styles.list}>{items}</ul>;
-};
-
-export default List;
diff --git a/src/components/WidgetParts/OrderedList/OrderedList.module.scss b/src/components/WidgetParts/OrderedList/OrderedList.module.scss
deleted file mode 100644
index a286932..0000000
--- a/src/components/WidgetParts/OrderedList/OrderedList.module.scss
+++ /dev/null
@@ -1,66 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/placeholders";
-
-.list {
- @extend %reset-ordered-list;
- counter-reset: link;
-
- .list {
- border-top: fun.convert-px(1) solid var(--color-primary-dark);
- }
-
- a {
- display: flex;
- flex-flow: row nowrap;
- width: 100%;
- padding: var(--spacing-2xs) var(--spacing-xs);
- background: none;
- text-decoration: underline solid transparent 0;
- transition: all 0.16s ease-in-out 0s, text-decoration-color 0s;
- counter-increment: link;
-
- &:hover,
- &:focus {
- background: var(--color-bg-secondary);
- }
-
- &:focus {
- color: var(--color-primary);
- text-decoration-color: var(--color-primary-light);
- text-decoration-thickness: 0.25ex;
- }
-
- &:active {
- background: var(--color-bg-tertiary);
- text-decoration-color: transparent;
- text-decoration-thickness: 0;
- }
-
- &::before {
- content: counters(link, ".") ". ";
- color: var(--color-secondary);
- padding-right: var(--spacing-2xs);
- }
- }
-
- li {
- width: 100%;
- margin: 0;
-
- &:not(:last-of-type) {
- border-bottom: fun.convert-px(1) solid var(--color-primary-dark);
- }
-
- &::before {
- display: none;
- }
- }
-
- li li a::before {
- padding-left: var(--spacing-sm);
- }
-
- li li li a::before {
- padding-left: var(--spacing-lg);
- }
-}
diff --git a/src/components/WidgetParts/OrderedList/OrderedList.tsx b/src/components/WidgetParts/OrderedList/OrderedList.tsx
deleted file mode 100644
index a12ec06..0000000
--- a/src/components/WidgetParts/OrderedList/OrderedList.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import styles from './OrderedList.module.scss';
-
-const OrderedList = ({ items }: { items: Array<any> }) => {
- return <ol className={styles.list}>{items}</ol>;
-};
-
-export default OrderedList;
diff --git a/src/components/WidgetParts/index.tsx b/src/components/WidgetParts/index.tsx
deleted file mode 100644
index 59df3bd..0000000
--- a/src/components/WidgetParts/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import ExpandableWidget from './ExpandableWidget/ExpandableWidget';
-import List from './List/List';
-import OrderedList from './OrderedList/OrderedList';
-
-export { ExpandableWidget, List, OrderedList };
diff --git a/src/components/Widgets/CVPreview/CVPreview.module.scss b/src/components/Widgets/CVPreview/CVPreview.module.scss
deleted file mode 100644
index 6ddd696..0000000
--- a/src/components/Widgets/CVPreview/CVPreview.module.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.preview {
- position: relative;
- width: 100%;
- height: 20rem;
- margin-bottom: var(--spacing-sm);
-}
diff --git a/src/components/Widgets/CVPreview/CVPreview.tsx b/src/components/Widgets/CVPreview/CVPreview.tsx
deleted file mode 100644
index cf6a8fa..0000000
--- a/src/components/Widgets/CVPreview/CVPreview.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ExpandableWidget } from '@components/WidgetParts';
-import Image from 'next/image';
-import Link from 'next/link';
-import { FormattedMessage } from 'react-intl';
-import styles from './CVPreview.module.scss';
-
-const CVPreview = ({
- title,
- imgSrc,
- pdf,
-}: {
- title: string;
- imgSrc: string;
- pdf: string;
-}) => {
- return (
- <ExpandableWidget title={title} expand={true}>
- <div className={styles.preview}>
- <Image
- src={imgSrc}
- layout="fill"
- objectFit="contain"
- objectPosition="left"
- alt="CV Armand Philippot"
- />
- </div>
- <p>
- <FormattedMessage
- defaultMessage="Download <link>CV in PDF</link>"
- description="CVPreview: download as PDF link"
- id="xC3Khf"
- values={{
- link: (chunks: string) => (
- <Link href={pdf}>
- <a>{chunks}</a>
- </Link>
- ),
- }}
- />
- </p>
- </ExpandableWidget>
- );
-};
-
-export default CVPreview;
diff --git a/src/components/Widgets/RecentPosts/RecentPosts.module.scss b/src/components/Widgets/RecentPosts/RecentPosts.module.scss
deleted file mode 100644
index 1b85265..0000000
--- a/src/components/Widgets/RecentPosts/RecentPosts.module.scss
+++ /dev/null
@@ -1,109 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/placeholders";
-
-.list {
- --items: 3;
- --items-size: 25ch;
-
- @extend %reset-list;
-
- display: grid;
- grid-template-columns: repeat(
- auto-fit,
- min(calc(100vw - (var(--spacing-md) * 2)), var(--items-size))
- );
- justify-content: center;
- gap: var(--spacing-sm);
- width: min(
- calc(100vw - (var(--spacing-md) * 2)),
- calc(
- (var(--items-size) * var(--items)) +
- (var(--spacing-sm) * (var(--items) - 1))
- )
- );
- margin-bottom: var(--spacing-md);
-}
-
-.item {
- text-align: center;
-}
-
-.article {
- display: flex;
- flex-flow: column nowrap;
- height: 100%;
- padding: 0 0 var(--spacing-md);
-}
-
-.title {
- flex: 1;
- margin: var(--spacing-sm) 0;
- padding: 0 var(--spacing-md);
- text-decoration: underline solid transparent 0;
- transition: all 0.3s linear 0s;
-}
-
-.link {
- display: block;
- height: 100%;
- background: var(--color-bg);
- color: inherit;
- text-decoration: none;
- border: fun.convert-px(3) solid var(--color-primary);
- border-radius: fun.convert-px(5);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow);
- transition: all 0.3s ease-in-out 0s;
-
- &:hover,
- &:focus,
- &:active {
- color: inherit;
- }
-
- &:hover,
- &:focus {
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow-light),
- fun.convert-px(7) fun.convert-px(10) fun.convert-px(12) fun.convert-px(-3)
- var(--color-shadow-light);
- transform: scale(1.05);
- }
-
- &:focus {
- .title {
- text-decoration: underline solid var(--color-primary) 0.3ex;
- }
- }
-
- &:active {
- box-shadow: 0 0 0 0 var(--color-shadow);
- transform: scale(0.95);
-
- .title {
- text-decoration: none;
- }
- }
-}
-
-.cover {
- width: 100%;
- height: clamp(fun.convert-px(100), 20vw, fun.convert-px(150));
- position: relative;
- border: fun.convert-px(1) solid var(--color-border);
-}
-
-.meta {
- display: block;
- margin: 0;
- padding: 0 var(--spacing-md);
- font-size: var(--font-size-sm);
-}
diff --git a/src/components/Widgets/RecentPosts/RecentPosts.tsx b/src/components/Widgets/RecentPosts/RecentPosts.tsx
deleted file mode 100644
index 11d8558..0000000
--- a/src/components/Widgets/RecentPosts/RecentPosts.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import Spinner from '@components/Spinner/Spinner';
-import { getPublishedPosts } from '@services/graphql/queries';
-import { ArticlePreview } from '@ts/types/articles';
-import { PostsList } from '@ts/types/blog';
-import { settings } from '@utils/config';
-import { getFormattedDate } from '@utils/helpers/format';
-import Image from 'next/image';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import useSWR from 'swr';
-import styles from './RecentPosts.module.scss';
-
-const RecentPosts = ({ posts }: { posts: PostsList }) => {
- const intl = useIntl();
- const { data, error } = useSWR<PostsList>(
- '/recent-posts',
- () => getPublishedPosts({ first: 3 }),
- { fallbackData: posts }
- );
- const router = useRouter();
- const locale = router.locale ? router.locale : settings.locales.defaultLocale;
-
- const getPost = (post: ArticlePreview) => {
- return (
- <li key={post.id} className={styles.item}>
- <Link href={`/article/${post.slug}`}>
- <a className={styles.link}>
- <article className={styles.article}>
- {post.featuredImage &&
- Object.keys(post.featuredImage).length > 0 && (
- <div className={styles.cover}>
- <Image
- src={post.featuredImage.sourceUrl}
- alt={post.featuredImage.altText}
- layout="fill"
- objectFit="contain"
- />
- </div>
- )}
- <h3 className={styles.title}>{post.title}</h3>
- <dl className={styles.meta}>
- <dt>
- {intl.formatMessage({
- defaultMessage: 'Published on:',
- description: 'RecentPosts: publication date label',
- id: '1h+N2z',
- })}
- </dt>
- <dd>
- <time dateTime={post.dates.publication}>
- {getFormattedDate(post.dates.publication, locale)}
- </time>
- </dd>
- </dl>
- </article>
- </a>
- </Link>
- </li>
- );
- };
-
- const getPostsItems = () => {
- if (error)
- return intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'RecentPosts: failed to load text',
- id: 'iyEh0R',
- });
- if (!data) return <Spinner />;
-
- return data.posts.map((post) => getPost(post));
- };
-
- return <ul className={styles.list}>{getPostsItems()}</ul>;
-};
-
-export default RecentPosts;
diff --git a/src/components/Widgets/RelatedThematics/RelatedThematics.tsx b/src/components/Widgets/RelatedThematics/RelatedThematics.tsx
deleted file mode 100644
index a66de82..0000000
--- a/src/components/Widgets/RelatedThematics/RelatedThematics.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { ExpandableWidget, List } from '@components/WidgetParts';
-import { ThematicPreview } from '@ts/types/taxonomies';
-import Link from 'next/link';
-import { useIntl } from 'react-intl';
-
-const RelatedThematics = ({ thematics }: { thematics: ThematicPreview[] }) => {
- const intl = useIntl();
- const sortedThematics = [...thematics].sort((a, b) =>
- a.title.localeCompare(b.title)
- );
-
- const thematicsList = sortedThematics.map((thematic) => {
- return (
- <li key={thematic.databaseId}>
- <Link href={`/thematique/${thematic.slug}`}>
- <a>{thematic.title}</a>
- </Link>
- </li>
- );
- });
-
- return (
- <ExpandableWidget
- expand={true}
- title={intl.formatMessage(
- {
- defaultMessage:
- '{thematicsCount, plural, =0 {Related thematics} one {Related thematic} other {Related thematics}}',
- description: 'RelatedThematics: widget title',
- id: 'qXQETZ',
- },
- { thematicsCount: thematics.length }
- )}
- withBorders={true}
- >
- <List items={thematicsList} />
- </ExpandableWidget>
- );
-};
-
-export default RelatedThematics;
diff --git a/src/components/Widgets/RelatedTopics/RelatedTopics.tsx b/src/components/Widgets/RelatedTopics/RelatedTopics.tsx
deleted file mode 100644
index 992173d..0000000
--- a/src/components/Widgets/RelatedTopics/RelatedTopics.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { ExpandableWidget, List } from '@components/WidgetParts';
-import { TopicPreview } from '@ts/types/taxonomies';
-import Link from 'next/link';
-import { useIntl } from 'react-intl';
-
-const RelatedTopics = ({ topics }: { topics: TopicPreview[] }) => {
- const intl = useIntl();
- const sortedTopics = [...topics].sort((a, b) =>
- a.title.localeCompare(b.title)
- );
-
- const topicsList = sortedTopics.map((topic) => {
- return (
- <li key={topic.databaseId}>
- <Link href={`/sujet/${topic.slug}`}>
- <a>{topic.title}</a>
- </Link>
- </li>
- );
- });
-
- return (
- <ExpandableWidget
- expand={true}
- title={intl.formatMessage(
- {
- defaultMessage:
- '{topicsCount, plural, =0 {Related topics} one {Related topic} other {Related topics}}',
- description: 'RelatedTopics: widget title',
- id: 'w/lPUh',
- },
- { topicsCount: topicsList.length }
- )}
- withBorders={true}
- >
- <List items={topicsList} />
- </ExpandableWidget>
- );
-};
-
-export default RelatedTopics;
diff --git a/src/components/Widgets/Sharing/Sharing.tsx b/src/components/Widgets/Sharing/Sharing.tsx
deleted file mode 100644
index 45fe3ce..0000000
--- a/src/components/Widgets/Sharing/Sharing.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import { ExpandableWidget } from '@components/WidgetParts';
-import { useRouter } from 'next/router';
-import { useEffect, useState } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './Sharing.module.scss';
-
-type Parameters = {
- content: string;
- image: string;
- title: string;
- url: string;
-};
-
-type Website = {
- id: string;
- name: string;
- parameters: Parameters;
- url: string;
-};
-
-const Sharing = ({ excerpt, title }: { excerpt: string; title: string }) => {
- const intl = useIntl();
- const [pageExcerpt, setPageExcerpt] = useState('');
- const [pageUrl, setPageUrl] = useState('');
- const [domainName, setDomainName] = useState('');
- const router = useRouter();
-
- useEffect(() => {
- const divEl = document.createElement('div');
- divEl.innerHTML = excerpt;
- const cleanExcerpt = divEl.textContent!;
- setPageExcerpt(cleanExcerpt);
- }, [excerpt]);
-
- useEffect(() => {
- const { protocol, hostname, port } = window.location;
- const currentPort = port ? `:${port}` : '';
- const fullUrl = `${protocol}//${hostname}${currentPort}${router.asPath}`;
-
- setDomainName(hostname);
- setPageUrl(fullUrl);
- }, [router.asPath]);
-
- const getSharingUrl = (website: Website): string => {
- const { id, parameters, url } = website;
- let sharingUrl = `${url}?`;
- let count = 0;
-
- for (const [key, value] of Object.entries(parameters)) {
- if (!value) continue;
-
- sharingUrl += count > 0 ? `&${value}=` : `${value}=`;
-
- switch (key) {
- case 'content':
- if (id === 'email') {
- const intro = intl.formatMessage({
- defaultMessage: 'Introduction:',
- description: 'Sharing: email content prefix',
- id: 'yfgMcl',
- });
- const readMore = intl.formatMessage({
- defaultMessage: 'Read more here:',
- description: 'Sharing: content link prefix',
- id: 'UsQske',
- });
- const body = `${intro}\n\n"${pageExcerpt}"\n\n${readMore} ${pageUrl}`;
- sharingUrl += encodeURI(body);
- } else {
- sharingUrl += encodeURI(pageExcerpt);
- }
- break;
- case 'title':
- const prefix =
- id === 'email'
- ? intl.formatMessage(
- {
- defaultMessage: 'Seen on {domainName}:',
- description: 'Sharing: seen on text',
- id: 'eUXMG4',
- },
- { domainName }
- )
- : '';
- sharingUrl += encodeURI(`${prefix} ${title}`);
- break;
- case 'url':
- sharingUrl += encodeURI(pageUrl);
- break;
- default:
- break;
- }
-
- count++;
- }
-
- return sharingUrl;
- };
-
- const websites = [
- {
- id: 'diaspora',
- name: intl.formatMessage({
- defaultMessage: 'Diaspora',
- description: 'Sharing: Diaspora',
- id: 'Dhow1m',
- }),
- parameters: {
- content: '',
- image: '',
- title: 'title',
- url: 'url',
- },
- url: 'https://share.diasporafoundation.org/',
- },
- {
- id: 'facebook',
- name: intl.formatMessage({
- defaultMessage: 'Facebook',
- description: 'Sharing: Facebook',
- id: '7iiaRx',
- }),
- parameters: {
- content: '',
- image: '',
- title: '',
- url: 'u',
- },
- url: 'https://www.facebook.com/sharer/sharer.php',
- },
- {
- id: 'linkedin',
- name: intl.formatMessage({
- defaultMessage: 'LinkedIn',
- description: 'Sharing: LinkedIn',
- id: 'csCQQk',
- }),
- parameters: {
- content: '',
- image: '',
- title: '',
- url: 'url',
- },
- url: 'https://www.linkedin.com/sharing/share-offsite/',
- },
- {
- id: 'twitter',
- name: intl.formatMessage({
- defaultMessage: 'Twitter',
- description: 'Sharing: Twitter',
- id: 'WjVBnY',
- }),
- parameters: {
- content: '',
- image: '',
- title: 'text',
- url: 'url',
- },
- url: 'https://twitter.com/intent/tweet',
- },
- {
- id: 'journal-du-hacker',
- name: intl.formatMessage({
- defaultMessage: 'Journal du hacker',
- description: 'Sharing: Journal du hacker',
- id: 'P0I+Xm',
- }),
- parameters: {
- content: '',
- image: '',
- title: 'title',
- url: 'url',
- },
- url: 'https://www.journalduhacker.net/stories/new',
- },
- {
- id: 'email',
- name: intl.formatMessage({
- defaultMessage: 'Email',
- description: 'Sharing: Email',
- id: 'lKZm9t',
- }),
- parameters: {
- content: 'body',
- image: '',
- title: 'subject',
- url: '',
- },
- url: 'mailto:',
- },
- ];
-
- const getItems = () => {
- return websites.map((website) => {
- const { id, name } = website;
- const sharingUrl = getSharingUrl(website);
- const linkModifier = `link--${id}`;
-
- return (
- <li key={id}>
- <a
- href={sharingUrl}
- title={name}
- className={`${styles.link} ${styles[linkModifier]}`}
- >
- <span className="screen-reader-text">
- {intl.formatMessage(
- {
- defaultMessage: 'Share on {name}',
- description: 'Sharing: share on social network text',
- id: 'ureXFw',
- },
- { name }
- )}
- </span>
- </a>
- </li>
- );
- });
- };
-
- return (
- <ExpandableWidget
- title={intl.formatMessage({
- defaultMessage: 'Share',
- description: 'Sharing: widget title',
- id: 'q3U6uI',
- })}
- expand={true}
- >
- <ul className={`${styles.list} ${styles['list--sharing']}`}>
- {getItems()}
- </ul>
- </ExpandableWidget>
- );
-};
-
-export default Sharing;
diff --git a/src/components/Widgets/SocialMedia/SocialMedia.tsx b/src/components/Widgets/SocialMedia/SocialMedia.tsx
deleted file mode 100644
index decf657..0000000
--- a/src/components/Widgets/SocialMedia/SocialMedia.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import GithubIcon from '@assets/images/social-media/github.svg';
-import GitlabIcon from '@assets/images/social-media/gitlab.svg';
-import LinkedInIcon from '@assets/images/social-media/linkedin.svg';
-import TwitterIcon from '@assets/images/social-media/twitter.svg';
-import styles from './SocialMedia.module.scss';
-import { ExpandableWidget } from '@components/WidgetParts';
-import { useIntl } from 'react-intl';
-
-const SocialMedia = ({
- title,
- github = false,
- gitlab = false,
- linkedin = false,
- twitter = false,
-}: {
- title: string;
- github?: boolean;
- gitlab?: boolean;
- linkedin?: boolean;
- twitter?: boolean;
-}) => {
- const intl = useIntl();
-
- const websites = [
- {
- id: 'github',
- name: intl.formatMessage({
- defaultMessage: 'Github',
- description: 'SocialMedia: Github',
- id: 'SWjj4l',
- }),
- url: 'https://github.com/ArmandPhilippot',
- },
- {
- id: 'gitlab',
- name: intl.formatMessage({
- defaultMessage: 'Gitlab',
- description: 'SocialMedia: Gitlab',
- id: 'obmlFh',
- }),
- url: 'https://gitlab.com/ArmandPhilippot',
- },
- {
- id: 'linkedin',
- name: intl.formatMessage({
- defaultMessage: 'LinkedIn',
- description: 'SocialMedia: LinkedIn',
- id: 'VbcHZ4',
- }),
- url: 'https://www.linkedin.com/in/armandphilippot',
- },
- {
- id: 'twitter',
- name: intl.formatMessage({
- defaultMessage: 'Twitter',
- description: 'SocialMedia: Twitter',
- id: 'IPs/Ck',
- }),
- url: 'https://twitter.com/ArmandPhilippot',
- },
- ];
-
- const getIcon = (id: string) => {
- switch (id) {
- case 'github':
- return <GithubIcon className={styles.icon} />;
- case 'gitlab':
- return <GitlabIcon className={styles.icon} />;
- case 'linkedin':
- return <LinkedInIcon className={styles.icon} />;
- case 'twitter':
- return <TwitterIcon className={styles.icon} />;
- default:
- break;
- }
- };
-
- const shouldDisplayLink = (id: string) => {
- switch (id) {
- case 'github':
- return github;
- case 'gitlab':
- return gitlab;
- case 'linkedin':
- return linkedin;
- case 'twitter':
- return twitter;
- default:
- break;
- }
- };
-
- const items = websites.map((website) => {
- return shouldDisplayLink(website.id) ? (
- <li key={website.id}>
- <a href={website.url} className={styles.link}>
- {getIcon(website.id)}
- <span className="screen-reader-text">{website.name}</span>
- </a>
- </li>
- ) : (
- ''
- );
- });
-
- return (
- <ExpandableWidget title={title} expand={true}>
- <ul className={styles.list}>{items}</ul>
- </ExpandableWidget>
- );
-};
-
-export default SocialMedia;
diff --git a/src/components/Widgets/ThematicsList/ThematicsList.tsx b/src/components/Widgets/ThematicsList/ThematicsList.tsx
deleted file mode 100644
index 51254ee..0000000
--- a/src/components/Widgets/ThematicsList/ThematicsList.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import Spinner from '@components/Spinner/Spinner';
-import { ExpandableWidget, List } from '@components/WidgetParts';
-import { getAllThematics } from '@services/graphql/queries';
-import { TitleLevel } from '@ts/types/app';
-import { ThematicPreview } from '@ts/types/taxonomies';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import useSWR from 'swr';
-
-const ThematicsList = ({
- title,
- titleLevel,
- initialData,
-}: {
- title: string;
- titleLevel?: TitleLevel;
- initialData?: ThematicPreview[];
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const isThematic = () => router.asPath.includes('/thematique/');
- const currentThematicSlug = isThematic()
- ? router.asPath.replace('/thematique/', '')
- : '';
-
- const { data, error } = useSWR('/api/thematics', getAllThematics, {
- fallbackData: initialData,
- });
-
- const getList = () => {
- if (error)
- return (
- <ul>
- {intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'ThematicsList: failed to load text',
- id: 'PxMDzL',
- })}
- </ul>
- );
- if (!data)
- return (
- <ul>
- <Spinner />
- </ul>
- );
-
- const thematics = data.map((thematic) => {
- return currentThematicSlug !== thematic.slug ? (
- <li key={thematic.databaseId}>
- <Link href={`/thematique/${thematic.slug}`}>
- <a>{thematic.title}</a>
- </Link>
- </li>
- ) : (
- ''
- );
- });
-
- return <List items={thematics} />;
- };
-
- return (
- <ExpandableWidget
- title={title}
- titleLevel={titleLevel}
- withBorders={true}
- expand={true}
- >
- {getList()}
- </ExpandableWidget>
- );
-};
-
-export default ThematicsList;
diff --git a/src/components/Widgets/ToC/ToC.tsx b/src/components/Widgets/ToC/ToC.tsx
deleted file mode 100644
index 3f759db..0000000
--- a/src/components/Widgets/ToC/ToC.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { ExpandableWidget, OrderedList } from '@components/WidgetParts';
-import { Heading } from '@ts/types/app';
-import useHeadingsTree from '@utils/hooks/useHeadingsTree';
-import { FormattedMessage, useIntl } from 'react-intl';
-
-const ToC = () => {
- const intl = useIntl();
- const headingsTree = useHeadingsTree('article');
- const title = intl.formatMessage({
- defaultMessage: 'Table of contents',
- description: 'ToC: widget title',
- id: 'Zg4L7U',
- });
-
- const getItems = (headings: Heading[]) => {
- return headings.map((heading) => {
- return (
- <li key={heading.id}>
- <a href={`#${heading.id}`}>
- <FormattedMessage
- defaultMessage="<a11y>Jump to </a11y>{title}"
- description="ToC: link"
- id="GgIWnN"
- values={{
- title: heading.title,
- a11y: (chunks: string) => (
- <span className="screen-reader-text">{chunks}</span>
- ),
- }}
- />
- </a>
- {heading.children.length > 0 && (
- <OrderedList items={getItems(heading.children)} />
- )}
- </li>
- );
- });
- };
-
- return (
- <ExpandableWidget title={title} kind="toc" expand={true} withBorders={true}>
- <noscript>
- {intl.formatMessage({
- defaultMessage:
- 'Javascript is required to use the table of contents.',
- description: 'ToC: noscript tag',
- id: 'RZzx/4',
- })}
- </noscript>
- <OrderedList items={getItems(headingsTree)} />
- </ExpandableWidget>
- );
-};
-
-export default ToC;
diff --git a/src/components/Widgets/TopicsList/TopicsList.tsx b/src/components/Widgets/TopicsList/TopicsList.tsx
deleted file mode 100644
index 7bc7d70..0000000
--- a/src/components/Widgets/TopicsList/TopicsList.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import Spinner from '@components/Spinner/Spinner';
-import { ExpandableWidget, List } from '@components/WidgetParts';
-import { getAllTopics } from '@services/graphql/queries';
-import { TitleLevel } from '@ts/types/app';
-import { TopicPreview } from '@ts/types/taxonomies';
-import Link from 'next/link';
-import { useRouter } from 'next/router';
-import { useIntl } from 'react-intl';
-import useSWR from 'swr';
-
-const TopicsList = ({
- title,
- titleLevel,
- initialData,
-}: {
- title: string;
- titleLevel?: TitleLevel;
- initialData?: TopicPreview[];
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const isTopic = () => router.asPath.includes('/sujet/');
- const currentTopicSlug = isTopic()
- ? router.asPath.replace('/sujet/', '')
- : '';
-
- const { data, error } = useSWR('/api/topics', getAllTopics, {
- fallbackData: initialData,
- });
-
- const getList = () => {
- if (error)
- return (
- <ul>
- {intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'TopicsList: failed to load text',
- id: '00Pf5p',
- })}
- </ul>
- );
- if (!data)
- return (
- <ul>
- <Spinner />
- </ul>
- );
-
- const topics = data.map((topic) => {
- return currentTopicSlug !== topic.slug ? (
- <li key={topic.databaseId}>
- <Link href={`/sujet/${topic.slug}`}>
- <a>{topic.title}</a>
- </Link>
- </li>
- ) : (
- ''
- );
- });
-
- return <List items={topics} />;
- };
-
- return (
- <ExpandableWidget
- title={title}
- titleLevel={titleLevel}
- withBorders={true}
- expand={true}
- >
- {getList()}
- </ExpandableWidget>
- );
-};
-
-export default TopicsList;
diff --git a/src/components/Widgets/index.tsx b/src/components/Widgets/index.tsx
deleted file mode 100644
index 8354449..0000000
--- a/src/components/Widgets/index.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import CVPreview from './CVPreview/CVPreview';
-import RecentPosts from './RecentPosts/RecentPosts';
-import RelatedThematics from './RelatedThematics/RelatedThematics';
-import RelatedTopics from './RelatedTopics/RelatedTopics';
-import Sharing from './Sharing/Sharing';
-import SocialMedia from './SocialMedia/SocialMedia';
-import ThematicsList from './ThematicsList/ThematicsList';
-import ToC from './ToC/ToC';
-import TopicsList from './TopicsList/TopicsList';
-
-export {
- CVPreview,
- RecentPosts,
- RelatedThematics,
- RelatedTopics,
- Sharing,
- SocialMedia,
- ThematicsList,
- ToC,
- TopicsList,
-};
diff --git a/src/components/atoms/buttons/button-link.stories.tsx b/src/components/atoms/buttons/button-link.stories.tsx
new file mode 100644
index 0000000..d06aff3
--- /dev/null
+++ b/src/components/atoms/buttons/button-link.stories.tsx
@@ -0,0 +1,139 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ButtonLink from './button-link';
+
+/**
+ * ButtonLink - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Buttons/ButtonLink',
+ component: ButtonLink,
+ args: {
+ shape: 'rectangle',
+ },
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible label.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button link.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ external: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the link is an external link.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The link kind.',
+ options: ['primary', 'secondary'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'secondary' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ shape: {
+ control: {
+ type: 'select',
+ },
+ description: 'The link shape.',
+ options: ['rectangle', 'square'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'rectangle' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ target: {
+ control: {
+ type: null,
+ },
+ description: 'The link target.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof ButtonLink>;
+
+const Template: ComponentStory<typeof ButtonLink> = (args) => (
+ <ButtonLink {...args} />
+);
+
+/**
+ * ButtonLink Story - Primary
+ */
+export const Primary = Template.bind({});
+Primary.args = {
+ children: 'Link',
+ kind: 'primary',
+ target: '#',
+};
+
+/**
+ * ButtonLink Story - Secondary
+ */
+export const Secondary = Template.bind({});
+Secondary.args = {
+ children: 'Link',
+ kind: 'secondary',
+ target: '#',
+};
+
+/**
+ * ButtonLink Story - Tertiary
+ */
+export const Tertiary = Template.bind({});
+Tertiary.args = {
+ children: 'Link',
+ kind: 'tertiary',
+ target: '#',
+};
diff --git a/src/components/atoms/buttons/button-link.test.tsx b/src/components/atoms/buttons/button-link.test.tsx
new file mode 100644
index 0000000..52ccdc7
--- /dev/null
+++ b/src/components/atoms/buttons/button-link.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import ButtonLink from './button-link';
+
+describe('ButtonLink', () => {
+ it('renders a ButtonLink component', () => {
+ render(<ButtonLink target="#">Button Link</ButtonLink>);
+ expect(screen.getByRole('link')).toHaveTextContent('Button Link');
+ });
+});
diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx
new file mode 100644
index 0000000..64e0afd
--- /dev/null
+++ b/src/components/atoms/buttons/button-link.tsx
@@ -0,0 +1,73 @@
+import Link from 'next/link';
+import { FC, ReactNode } from 'react';
+import styles from './buttons.module.scss';
+
+export type ButtonLinkProps = {
+ /**
+ * ButtonLink accessible label.
+ */
+ 'aria-label'?: string;
+ /**
+ * The button link body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames to the button link.
+ */
+ className?: string;
+ /**
+ * True if it is an external link. Default: false.
+ */
+ external?: boolean;
+ /**
+ * ButtonLink kind. Default: secondary.
+ */
+ kind?: 'primary' | 'secondary' | 'tertiary';
+ /**
+ * ButtonLink shape. Default: rectangle.
+ */
+ shape?: 'circle' | 'rectangle' | 'square';
+ /**
+ * Define an URL as target.
+ */
+ target: string;
+};
+
+/**
+ * ButtonLink component
+ *
+ * Use a button-like link as call to action.
+ */
+const ButtonLink: FC<ButtonLinkProps> = ({
+ children,
+ className,
+ target,
+ kind = 'secondary',
+ shape = 'rectangle',
+ external = false,
+ ...props
+}) => {
+ const kindClass = styles[`btn--${kind}`];
+ const shapeClass = styles[`btn--${shape}`];
+
+ return external ? (
+ <a
+ href={target}
+ className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
+ {...props}
+ >
+ {children}
+ </a>
+ ) : (
+ <Link href={target}>
+ <a
+ className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
+ {...props}
+ >
+ {children}
+ </a>
+ </Link>
+ );
+};
+
+export default ButtonLink;
diff --git a/src/components/atoms/buttons/button.stories.tsx b/src/components/atoms/buttons/button.stories.tsx
new file mode 100644
index 0000000..6803706
--- /dev/null
+++ b/src/components/atoms/buttons/button.stories.tsx
@@ -0,0 +1,172 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Button from './button';
+
+/**
+ * Button - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Buttons/Button',
+ component: Button,
+ args: {
+ disabled: false,
+ type: 'button',
+ },
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible label.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The button body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Render button as disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'Button kind.',
+ options: ['primary', 'secondary', 'tertiary', 'neutral'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'secondary' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ onClick: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to handle click.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ shape: {
+ control: {
+ type: 'select',
+ },
+ description: 'The link shape.',
+ options: ['circle', 'rectangle', 'square', 'initial'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'rectangle' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ type: {
+ control: {
+ type: 'select',
+ },
+ description: 'Button type attribute.',
+ options: ['button', 'reset', 'submit'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'button' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Button>;
+
+const Template: ComponentStory<typeof Button> = (args) => {
+ const { children, type, ...props } = args;
+
+ const getBody = () => {
+ if (children) return children;
+
+ switch (type) {
+ case 'reset':
+ return 'Reset';
+ case 'submit':
+ return 'Submit';
+ case 'button':
+ default:
+ return 'Button';
+ }
+ };
+
+ return (
+ <Button type={type} {...props}>
+ {getBody()}
+ </Button>
+ );
+};
+
+/**
+ * Button Story - Primary
+ */
+export const Primary = Template.bind({});
+Primary.args = {
+ kind: 'primary',
+};
+
+/**
+ * Button Story - Secondary
+ */
+export const Secondary = Template.bind({});
+Secondary.args = {
+ kind: 'secondary',
+};
+
+/**
+ * Button Story - Tertiary
+ */
+export const Tertiary = Template.bind({});
+Tertiary.args = {
+ kind: 'tertiary',
+};
diff --git a/src/components/atoms/buttons/button.test.tsx b/src/components/atoms/buttons/button.test.tsx
new file mode 100644
index 0000000..57c79c6
--- /dev/null
+++ b/src/components/atoms/buttons/button.test.tsx
@@ -0,0 +1,18 @@
+import { render, screen } from '@test-utils';
+import Button from './button';
+
+describe('Button', () => {
+ it('renders the Button component', () => {
+ render(<Button onClick={() => null}>Button</Button>);
+ expect(screen.getByRole('button')).toBeInTheDocument();
+ });
+
+ it('renders the Button component with disabled state', () => {
+ render(
+ <Button onClick={() => null} disabled={true}>
+ Disabled Button
+ </Button>
+ );
+ expect(screen.getByRole('button')).toBeDisabled();
+ });
+});
diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
new file mode 100644
index 0000000..9776687
--- /dev/null
+++ b/src/components/atoms/buttons/button.tsx
@@ -0,0 +1,77 @@
+import {
+ forwardRef,
+ ForwardRefRenderFunction,
+ MouseEventHandler,
+ ReactNode,
+} from 'react';
+import styles from './buttons.module.scss';
+
+export type ButtonProps = {
+ /**
+ * The button body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames to the button wrapper.
+ */
+ className?: string;
+ /**
+ * Button accessible label.
+ */
+ 'aria-label'?: string;
+ /**
+ * Button state. Default: false.
+ */
+ disabled?: boolean;
+ /**
+ * Button kind. Default: secondary.
+ */
+ kind?: 'primary' | 'secondary' | 'tertiary' | 'neutral';
+ /**
+ * A callback function to handle click.
+ */
+ onClick?: MouseEventHandler<HTMLButtonElement>;
+ /**
+ * Button shape. Default: rectangle.
+ */
+ shape?: 'circle' | 'rectangle' | 'square' | 'initial';
+ /**
+ * Button type attribute. Default: button.
+ */
+ type?: 'button' | 'reset' | 'submit';
+};
+
+/**
+ * Button component
+ *
+ * Use a button as call to action.
+ */
+const Button: ForwardRefRenderFunction<HTMLButtonElement, ButtonProps> = (
+ {
+ className = '',
+ children,
+ disabled = false,
+ kind = 'secondary',
+ shape = 'rectangle',
+ type = 'button',
+ ...props
+ },
+ ref
+) => {
+ const kindClass = styles[`btn--${kind}`];
+ const shapeClass = styles[`btn--${shape}`];
+
+ return (
+ <button
+ className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
+ disabled={disabled}
+ ref={ref}
+ type={type}
+ {...props}
+ >
+ {children}
+ </button>
+ );
+};
+
+export default forwardRef(Button);
diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss
new file mode 100644
index 0000000..2444bb1
--- /dev/null
+++ b/src/components/atoms/buttons/buttons.module.scss
@@ -0,0 +1,177 @@
+@use "@styles/abstracts/functions" as fun;
+
+.btn {
+ display: inline-flex;
+ place-content: center;
+ align-items: center;
+ border: none;
+ border-radius: fun.convert-px(5);
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ text-decoration: none;
+ transition: all 0.3s ease-in-out 0s;
+
+ &--initial {
+ border-radius: 0;
+ }
+
+ &--rectangle {
+ padding: var(--spacing-2xs) var(--spacing-sm);
+ }
+
+ &--square,
+ &--circle {
+ padding: var(--spacing-xs);
+ aspect-ratio: 1 / 1;
+ }
+
+ &--circle {
+ border-radius: 50%;
+ }
+
+ &:disabled {
+ cursor: wait;
+ }
+
+ &--neutral {
+ background: inherit;
+ }
+
+ &--primary {
+ background: var(--color-primary);
+ border: fun.convert-px(2) solid var(--color-bg);
+ box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary),
+ 0 0 0 fun.convert-px(3) var(--color-primary-darker),
+ fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(3)
+ var(--color-primary-dark);
+ color: var(--color-fg-inverted);
+ text-shadow: fun.convert-px(2) fun.convert-px(2) 0 var(--color-shadow);
+
+ &:disabled {
+ background: var(--color-primary-darker);
+ }
+
+ &:not(:disabled) {
+ &:hover,
+ &:focus {
+ background: var(--color-primary-light);
+ box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary-light),
+ 0 0 0 fun.convert-px(3) var(--color-primary-darker),
+ fun.convert-px(7) fun.convert-px(7) 0 fun.convert-px(2)
+ var(--color-primary-dark);
+ color: var(--color-fg-inverted);
+ transform: translateX(#{fun.convert-px(-4)})
+ translateY(#{fun.convert-px(-4)});
+ }
+
+ &:focus {
+ text-decoration: underline solid var(--color-fg-inverted)
+ fun.convert-px(2);
+ }
+
+ &:active {
+ background: var(--color-primary-dark);
+ box-shadow: 0 0 0 fun.convert-px(2) var(--color-primary),
+ 0 0 0 fun.convert-px(3) var(--color-primary-darker),
+ 0 0 0 0 var(--color-primary-dark);
+ text-decoration: none;
+ transform: translateX(#{fun.convert-px(4)})
+ translateY(#{fun.convert-px(4)});
+ }
+ }
+ }
+
+ &--secondary {
+ background: var(--color-bg);
+ border: fun.convert-px(3) solid var(--color-primary);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
+ var(--color-shadow),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
+ var(--color-shadow);
+ color: var(--color-primary);
+
+ &:disabled {
+ border-color: var(--color-border-dark);
+ color: var(--color-fg-light);
+ }
+
+ &:not(:disabled) {
+ &:hover,
+ &:focus {
+ border-color: var(--color-primary-light);
+ color: var(--color-primary-light);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow-light),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2)
+ fun.convert-px(-2) var(--color-shadow-light),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5)
+ fun.convert-px(-4) var(--color-shadow-light),
+ fun.convert-px(7) fun.convert-px(10) fun.convert-px(12)
+ fun.convert-px(-3) var(--color-shadow-light);
+ transform: scale(var(--scale-up, 1.1));
+ }
+
+ &:focus {
+ text-decoration: underline var(--color-primary-light) fun.convert-px(3);
+ }
+
+ &:active {
+ border-color: var(--color-primary-dark);
+ box-shadow: 0 0 0 0 var(--color-shadow);
+ color: var(--color-primary-dark);
+ text-decoration: none;
+ transform: scale(var(--scale-down, 0.94));
+ }
+ }
+ }
+
+ &--tertiary {
+ background: var(--color-bg);
+ border: fun.convert-px(3) solid var(--color-primary);
+ box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg),
+ fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-dark),
+ fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg),
+ fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-dark);
+ color: var(--color-primary);
+
+ &:disabled {
+ color: var(--color-fg-light);
+ border-color: var(--color-border-dark);
+ box-shadow: fun.convert-px(2) fun.convert-px(2) 0 0 var(--color-bg),
+ fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-primary-darker),
+ fun.convert-px(5) fun.convert-px(5) 0 0 var(--color-bg),
+ fun.convert-px(6) fun.convert-px(6) 0 0 var(--color-primary-darker);
+ }
+
+ &:not(:disabled) {
+ &:hover,
+ &:focus {
+ border-color: var(--color-primary-light);
+ box-shadow: fun.convert-px(2) fun.convert-px(3) 0 0 var(--color-bg),
+ fun.convert-px(4) fun.convert-px(5) 0 0 var(--color-primary),
+ fun.convert-px(6) fun.convert-px(8) 0 0 var(--color-bg),
+ fun.convert-px(8) fun.convert-px(10) 0 0 var(--color-primary),
+ fun.convert-px(10) fun.convert-px(12) fun.convert-px(1) 0
+ var(--color-shadow-light),
+ fun.convert-px(10) fun.convert-px(12) fun.convert-px(5)
+ fun.convert-px(1) var(--color-shadow-light);
+ color: var(--color-primary-light);
+ transform: translateX(#{fun.convert-px(-3)})
+ translateY(#{fun.convert-px(-5)});
+ }
+
+ &:focus {
+ text-decoration: underline var(--color-primary) fun.convert-px(2);
+ }
+
+ &:active {
+ box-shadow: 0 0 0 0 var(--color-shadow);
+ text-decoration: none;
+ transform: translateX(#{fun.convert-px(5)})
+ translateY(#{fun.convert-px(6)});
+ }
+ }
+ }
+}
diff --git a/src/components/atoms/forms/checkbox.stories.tsx b/src/components/atoms/forms/checkbox.stories.tsx
new file mode 100644
index 0000000..588fdcc
--- /dev/null
+++ b/src/components/atoms/forms/checkbox.stories.tsx
@@ -0,0 +1,102 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import CheckboxComponent from './checkbox';
+
+/**
+ * Checkbox - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Forms',
+ component: CheckboxComponent,
+ argTypes: {
+ 'aria-labelledby': {
+ control: {
+ type: 'text',
+ },
+ description: 'One or more ids that refers to the checkbox name.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the checkbox.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'The checkbox id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'The checkbox name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to handle checkbox state.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description:
+ 'The checkbox state: either checked (true) or unchecked (false).',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof CheckboxComponent>;
+
+const Template: ComponentStory<typeof CheckboxComponent> = ({
+ value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [isChecked, setIsChecked] = useState<boolean>(value);
+
+ return (
+ <CheckboxComponent value={isChecked} setValue={setIsChecked} {...args} />
+ );
+};
+
+/**
+ * Checkbox Story
+ */
+export const Checkbox = Template.bind({});
+Checkbox.args = {
+ id: 'storybook-checkbox',
+ name: 'storybook-checkbox',
+ value: false,
+};
diff --git a/src/components/atoms/forms/checkbox.test.tsx b/src/components/atoms/forms/checkbox.test.tsx
new file mode 100644
index 0000000..3b54549
--- /dev/null
+++ b/src/components/atoms/forms/checkbox.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen } from '@test-utils';
+import Checkbox from './checkbox';
+
+describe('Checkbox', () => {
+ it('renders an unchecked checkbox', () => {
+ render(
+ <Checkbox
+ id="jest-checkbox"
+ name="jest-checkbox"
+ value={false}
+ setValue={() => null}
+ />
+ );
+ expect(screen.getByRole('checkbox')).not.toBeChecked();
+ });
+
+ it('renders a checked checkbox', () => {
+ render(
+ <Checkbox
+ id="jest-checkbox"
+ name="jest-checkbox"
+ value={true}
+ setValue={() => null}
+ />
+ );
+ expect(screen.getByRole('checkbox')).toBeChecked();
+ });
+});
diff --git a/src/components/atoms/forms/checkbox.tsx b/src/components/atoms/forms/checkbox.tsx
new file mode 100644
index 0000000..aec97f0
--- /dev/null
+++ b/src/components/atoms/forms/checkbox.tsx
@@ -0,0 +1,46 @@
+import { FC, SetStateAction } from 'react';
+
+export type CheckboxProps = {
+ /**
+ * One or more ids that refers to the checkbox name.
+ */
+ 'aria-labelledby'?: string;
+ /**
+ * Add classnames to the checkbox.
+ */
+ className?: string;
+ /**
+ * Checkbox id attribute.
+ */
+ id: string;
+ /**
+ * Checkbox name attribute.
+ */
+ name: string;
+ /**
+ * Callback function to set checkbox value.
+ */
+ setValue: (value: SetStateAction<boolean>) => void;
+ /**
+ * Checkbox value.
+ */
+ value: boolean;
+};
+
+/**
+ * Checkbox component
+ *
+ * Render a checkbox type input.
+ */
+const Checkbox: FC<CheckboxProps> = ({ value, setValue, ...props }) => {
+ return (
+ <input
+ type="checkbox"
+ checked={value}
+ onChange={() => setValue(!value)}
+ {...props}
+ />
+ );
+};
+
+export default Checkbox;
diff --git a/src/components/atoms/forms/field.stories.tsx b/src/components/atoms/forms/field.stories.tsx
new file mode 100644
index 0000000..00a183d
--- /dev/null
+++ b/src/components/atoms/forms/field.stories.tsx
@@ -0,0 +1,257 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import Field from './field';
+
+/**
+ * Field - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Forms/Fields',
+ component: Field,
+ args: {
+ disabled: false,
+ required: false,
+ },
+ argTypes: {
+ 'aria-labelledby': {
+ control: {
+ type: 'text',
+ },
+ description: 'One or more ids that refers to the field name.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add classnames to the field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Field state: either enabled or disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ max: {
+ control: {
+ type: 'number',
+ },
+ description: 'Maximum value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ min: {
+ control: {
+ type: 'number',
+ },
+ description: 'Minimum value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ placeholder: {
+ control: {
+ type: 'text',
+ },
+ description: 'A placeholder value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ required: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'Callback function to set field value.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ step: {
+ control: {
+ type: 'number',
+ },
+ description: 'Field incremental values that are valid.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ type: {
+ control: {
+ type: 'select',
+ },
+ description: 'Field type: input type or textarea.',
+ options: [
+ 'datetime-local',
+ 'email',
+ 'number',
+ 'search',
+ 'tel',
+ 'text',
+ 'textarea',
+ 'time',
+ 'url',
+ ],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'Field value.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Field>;
+
+const Template: ComponentStory<typeof Field> = ({
+ value: _value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [value, setValue] = useState<string>('');
+
+ return <Field value={value} setValue={setValue} {...args} />;
+};
+
+/**
+ * Field Story - DateTime
+ */
+export const DateTime = Template.bind({});
+DateTime.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'datetime-local',
+};
+
+/**
+ * Field Story - Email
+ */
+export const Email = Template.bind({});
+Email.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'email',
+};
+
+/**
+ * Field Story - Text
+ */
+export const Text = Template.bind({});
+Text.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'text',
+};
+
+/**
+ * Field Story - Number
+ */
+export const Number = Template.bind({});
+Number.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'number',
+};
+
+/**
+ * Field Story - TextArea
+ */
+export const TextArea = Template.bind({});
+TextArea.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'textarea',
+};
+
+/**
+ * Field Story - Time
+ */
+export const Time = Template.bind({});
+Time.args = {
+ id: 'field-storybook',
+ name: 'field-storybook',
+ type: 'time',
+};
diff --git a/src/components/atoms/forms/field.test.tsx b/src/components/atoms/forms/field.test.tsx
new file mode 100644
index 0000000..a04a976
--- /dev/null
+++ b/src/components/atoms/forms/field.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from '@test-utils';
+import Field from './field';
+
+describe('Field', () => {
+ it('renders a text input', () => {
+ render(
+ <Field
+ id="text-field"
+ name="text-field"
+ type="text"
+ value=""
+ setValue={() => null}
+ />
+ );
+ expect(screen.getByRole('textbox')).toHaveAttribute('type', 'text');
+ });
+
+ it('renders a search input', () => {
+ render(
+ <Field
+ id="search-field"
+ name="search-field"
+ type="search"
+ value=""
+ setValue={() => null}
+ />
+ );
+ expect(screen.getByRole('searchbox')).toHaveAttribute('type', 'search');
+ });
+});
diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx
new file mode 100644
index 0000000..377e1b0
--- /dev/null
+++ b/src/components/atoms/forms/field.tsx
@@ -0,0 +1,111 @@
+import {
+ ChangeEvent,
+ forwardRef,
+ ForwardRefRenderFunction,
+ SetStateAction,
+} from 'react';
+import styles from './forms.module.scss';
+
+export type FieldType =
+ | 'datetime-local'
+ | 'email'
+ | 'number'
+ | 'search'
+ | 'tel'
+ | 'text'
+ | 'textarea'
+ | 'time'
+ | 'url';
+
+export type FieldProps = {
+ /**
+ * One or more ids that refers to the field name.
+ */
+ 'aria-labelledby'?: string;
+ /**
+ * Add classnames to the field.
+ */
+ className?: string;
+ /**
+ * Field state. Either enabled (false) or disabled (true).
+ */
+ disabled?: boolean;
+ /**
+ * Field id attribute.
+ */
+ id: string;
+ /**
+ * Field maximum value.
+ */
+ max?: number | string;
+ /**
+ * Field minimum value.
+ */
+ min?: number | string;
+ /**
+ * Field name attribute.
+ */
+ name: string;
+ /**
+ * Placeholder value.
+ */
+ placeholder?: string;
+ /**
+ * True if the field is required. Default: false.
+ */
+ required?: boolean;
+ /**
+ * Callback function to set field value.
+ */
+ setValue: (value: SetStateAction<string>) => void;
+ /**
+ * Field incremental values that are valid.
+ */
+ step?: number | string;
+ /**
+ * Field type. Default: text.
+ */
+ type: FieldType;
+ /**
+ * Field value.
+ */
+ value: string;
+};
+
+/**
+ * Field component.
+ *
+ * Render either an input or a textarea.
+ */
+const Field: ForwardRefRenderFunction<HTMLInputElement, FieldProps> = (
+ { className = '', setValue, type, ...props },
+ ref
+) => {
+ /**
+ * Update select value when an option is selected.
+ * @param e - The option change event.
+ */
+ const updateValue = (
+ e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
+ ) => {
+ setValue(e.target.value);
+ };
+
+ return type === 'textarea' ? (
+ <textarea
+ onChange={updateValue}
+ className={`${styles.field} ${styles['field--textarea']} ${className}`}
+ {...props}
+ />
+ ) : (
+ <input
+ className={`${styles.field} ${className}`}
+ onChange={updateValue}
+ ref={ref}
+ type={type}
+ {...props}
+ />
+ );
+};
+
+export default forwardRef(Field);
diff --git a/src/components/atoms/forms/form.test.tsx b/src/components/atoms/forms/form.test.tsx
new file mode 100644
index 0000000..8b534f1
--- /dev/null
+++ b/src/components/atoms/forms/form.test.tsx
@@ -0,0 +1,13 @@
+import { render, screen } from '@test-utils';
+import Form from './form';
+
+describe('Form', () => {
+ it('renders a form', () => {
+ render(
+ <Form aria-label="Jest form" onSubmit={() => null}>
+ Fields
+ </Form>
+ );
+ expect(screen.getByRole('form', { name: 'Jest form' })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx
new file mode 100644
index 0000000..b819aea
--- /dev/null
+++ b/src/components/atoms/forms/form.tsx
@@ -0,0 +1,76 @@
+import { Children, FC, FormEvent, Fragment, ReactNode } from 'react';
+import styles from './forms.module.scss';
+
+export type FormProps = {
+ /**
+ * An accessible name.
+ */
+ 'aria-label'?: string;
+ /**
+ * One or more ids that refers to the form name.
+ */
+ 'aria-labelledby'?: string;
+ /**
+ * The form body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames to the form wrapper.
+ */
+ className?: string;
+ /**
+ * Wrap each items with a div. Default: true.
+ */
+ grouped?: boolean;
+ /**
+ * A callback function to execute on submit.
+ */
+ onSubmit: () => void;
+};
+
+/**
+ * Form component.
+ *
+ * Render children wrapped in a form element.
+ */
+const Form: FC<FormProps> = ({
+ children,
+ grouped = true,
+ onSubmit,
+ ...props
+}) => {
+ const arrayChildren = Children.toArray(children);
+
+ /**
+ * Get the form items.
+ * @returns {JSX.Element[]} An array of child elements wrapped in a div.
+ */
+ const getFormItems = (): JSX.Element[] => {
+ return arrayChildren.map((child, index) =>
+ grouped ? (
+ <div key={`item-${index}`} className={styles.item}>
+ {child}
+ </div>
+ ) : (
+ <Fragment key={`item-${index}`}>{child}</Fragment>
+ )
+ );
+ };
+
+ /**
+ * Handle form submit.
+ * @param {FormEvent} e - The form event.
+ */
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+ onSubmit();
+ };
+
+ return (
+ <form onSubmit={handleSubmit} {...props}>
+ {getFormItems()}
+ </form>
+ );
+};
+
+export default Form;
diff --git a/src/components/atoms/forms/forms.module.scss b/src/components/atoms/forms/forms.module.scss
new file mode 100644
index 0000000..19c7aee
--- /dev/null
+++ b/src/components/atoms/forms/forms.module.scss
@@ -0,0 +1,53 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.item {
+ margin: var(--spacing-xs) 0;
+ width: 100%;
+ max-width: 45ch;
+}
+
+.field {
+ padding: var(--spacing-2xs) var(--spacing-xs);
+ background: var(--color-bg-tertiary);
+ border: fun.convert-px(2) solid var(--color-border);
+ box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow);
+ transition: all 0.25s linear 0s;
+
+ &--select {
+ cursor: pointer;
+
+ @include mix.pointer("fine") {
+ padding: fun.convert-px(3) var(--spacing-xs);
+ }
+ }
+
+ &--textarea {
+ min-height: fun.convert-px(200);
+ }
+
+ &:disabled {
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(2) solid var(--color-border-light);
+ box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0
+ var(--color-shadow-light);
+ cursor: not-allowed;
+ }
+
+ &:not(:disabled) {
+ &:hover {
+ box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1)
+ var(--color-shadow);
+ transform: translate(#{fun.convert-px(-3)}, #{fun.convert-px(-3)});
+ }
+
+ &:focus {
+ background: var(--color-bg);
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 0 var(--color-shadow);
+ transform: translate(#{fun.convert-px(3)}, #{fun.convert-px(3)});
+ outline: none;
+ transition: all 0.2s ease-in-out 0s, transform 0.3s ease-out 0s;
+ }
+ }
+}
diff --git a/src/components/atoms/forms/label.module.scss b/src/components/atoms/forms/label.module.scss
new file mode 100644
index 0000000..f900925
--- /dev/null
+++ b/src/components/atoms/forms/label.module.scss
@@ -0,0 +1,17 @@
+.label {
+ color: var(--color-primary-darker);
+ font-weight: 600;
+
+ &--small {
+ font-size: var(--font-size-sm);
+ font-variant: small-caps;
+ }
+
+ &--medium {
+ font-size: var(--font-size-md);
+ }
+}
+
+.required {
+ color: var(--color-secondary);
+}
diff --git a/src/components/atoms/forms/label.stories.tsx b/src/components/atoms/forms/label.stories.tsx
new file mode 100644
index 0000000..f66aa13
--- /dev/null
+++ b/src/components/atoms/forms/label.stories.tsx
@@ -0,0 +1,104 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import LabelComponent from './label';
+
+/**
+ * Label - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Forms',
+ component: LabelComponent,
+ args: {
+ required: false,
+ size: 'small',
+ },
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'text',
+ },
+ description: 'Define an accessible name.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add classnames to the label.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The label body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ htmlFor: {
+ control: {
+ type: 'text',
+ },
+ description: 'The field id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ required: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Set to true if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ size: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label size.',
+ options: ['medium', 'small'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'small' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof LabelComponent>;
+
+const Template: ComponentStory<typeof LabelComponent> = ({
+ children,
+ ...args
+}) => <LabelComponent {...args}>{children}</LabelComponent>;
+
+/**
+ * Label Story
+ */
+export const Label = Template.bind({});
+Label.args = {
+ children: 'A label',
+};
diff --git a/src/components/atoms/forms/label.test.tsx b/src/components/atoms/forms/label.test.tsx
new file mode 100644
index 0000000..14257c3
--- /dev/null
+++ b/src/components/atoms/forms/label.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import Label from './label';
+
+describe('Label', () => {
+ it('renders a field label', () => {
+ render(<Label>A label</Label>);
+ expect(screen.getByText('A label')).toBeDefined();
+ });
+});
diff --git a/src/components/atoms/forms/label.tsx b/src/components/atoms/forms/label.tsx
new file mode 100644
index 0000000..2ec614f
--- /dev/null
+++ b/src/components/atoms/forms/label.tsx
@@ -0,0 +1,53 @@
+import { FC, ReactNode } from 'react';
+import styles from './label.module.scss';
+
+export type LabelProps = {
+ /**
+ * An accessible name for the label.
+ */
+ 'aria-label'?: string;
+ /**
+ * The label body.
+ */
+ children: ReactNode;
+ /**
+ * Add classnames to the label.
+ */
+ className?: string;
+ /**
+ * The field id.
+ */
+ htmlFor?: string;
+ /**
+ * Is the field required? Default: false.
+ */
+ required?: boolean;
+ /**
+ * The label size. Default: small.
+ */
+ size?: 'medium' | 'small';
+};
+
+/**
+ * Label Component
+ *
+ * Render a HTML label element.
+ */
+const Label: FC<LabelProps> = ({
+ children,
+ className = '',
+ required = false,
+ size = 'small',
+ ...props
+}) => {
+ const sizeClass = styles[`label--${size}`];
+
+ return (
+ <label className={`${styles.label} ${sizeClass} ${className}`} {...props}>
+ {children}
+ {required && <span className={styles.required}> *</span>}
+ </label>
+ );
+};
+
+export default Label;
diff --git a/src/components/atoms/forms/select.stories.tsx b/src/components/atoms/forms/select.stories.tsx
new file mode 100644
index 0000000..7127597
--- /dev/null
+++ b/src/components/atoms/forms/select.stories.tsx
@@ -0,0 +1,151 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import SelectComponent from './select';
+
+const selectOptions = [
+ { id: 'option1', name: 'Option 1', value: 'option1' },
+ { id: 'option2', name: 'Option 2', value: 'option2' },
+ { id: 'option3', name: 'Option 3', value: 'option3' },
+];
+
+/**
+ * Select - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Forms',
+ component: SelectComponent,
+ args: {
+ disabled: false,
+ required: false,
+ },
+ argTypes: {
+ 'aria-labelledby': {
+ control: {
+ type: 'text',
+ },
+ description: 'One or more ids that refers to the select field name.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add classnames to the select field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Field state: either enabled or disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ options: {
+ description: 'Select options.',
+ type: {
+ name: 'array',
+ required: true,
+ value: {
+ name: 'string',
+ },
+ },
+ },
+ required: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'Callback function to set field value.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'Field value.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SelectComponent>;
+
+const Template: ComponentStory<typeof SelectComponent> = ({
+ value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [selected, setSelected] = useState<string>(value);
+
+ return <SelectComponent value={selected} setValue={setSelected} {...args} />;
+};
+
+/**
+ * Select Story
+ */
+export const Select = Template.bind({});
+Select.args = {
+ id: 'storybook-select',
+ name: 'storybook-select',
+ options: selectOptions,
+ value: 'option2',
+};
diff --git a/src/components/atoms/forms/select.test.tsx b/src/components/atoms/forms/select.test.tsx
new file mode 100644
index 0000000..22efb86
--- /dev/null
+++ b/src/components/atoms/forms/select.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from '@test-utils';
+import Select from './select';
+
+const selectOptions = [
+ { id: 'option1', name: 'Option 1', value: 'option1' },
+ { id: 'option2', name: 'Option 2', value: 'option2' },
+ { id: 'option3', name: 'Option 3', value: 'option3' },
+];
+const selected = selectOptions[0];
+
+describe('Select', () => {
+ it('should correctly set default option', () => {
+ render(
+ <Select
+ id="jest-select"
+ name="jest-select"
+ options={selectOptions}
+ value={selected.value}
+ setValue={() => null}
+ />
+ );
+ expect(screen.getByRole('combobox')).toHaveValue(selected.value);
+ expect(screen.queryByRole('combobox')).not.toHaveValue(
+ selectOptions[1].value
+ );
+ expect(screen.queryByRole('combobox')).not.toHaveValue(
+ selectOptions[2].value
+ );
+ });
+});
diff --git a/src/components/atoms/forms/select.tsx b/src/components/atoms/forms/select.tsx
new file mode 100644
index 0000000..dbe9b37
--- /dev/null
+++ b/src/components/atoms/forms/select.tsx
@@ -0,0 +1,99 @@
+import { ChangeEvent, FC, SetStateAction } from 'react';
+import styles from './forms.module.scss';
+
+export type SelectOptions = {
+ /**
+ * The option id.
+ */
+ id: string;
+ /**
+ * The option name.
+ */
+ name: string;
+ /**
+ * The option value.
+ */
+ value: string;
+};
+
+export type SelectProps = {
+ /**
+ * One or more ids that refers to the select field name.
+ */
+ 'aria-labelledby'?: string;
+ /**
+ * Add classnames to the select field.
+ */
+ className?: string;
+ /**
+ * Field state. Either enabled (false) or disabled (true).
+ */
+ disabled?: boolean;
+ /**
+ * Field id attribute.
+ */
+ id: string;
+ /**
+ * Field name attribute.
+ */
+ name: string;
+ /**
+ * True if the field is required. Default: false.
+ */
+ options: SelectOptions[];
+ /**
+ * True if the field is required. Default: false.
+ */
+ required?: boolean;
+ /**
+ * Callback function to set field value.
+ */
+ setValue: (value: SetStateAction<string>) => void;
+ /**
+ * Field value.
+ */
+ value: string;
+};
+
+/**
+ * Select component
+ *
+ * Render a HTML select element.
+ */
+const Select: FC<SelectProps> = ({
+ className = '',
+ options,
+ setValue,
+ ...props
+}) => {
+ /**
+ * Update select value when an option is selected.
+ * @param e - The option change event.
+ */
+ const updateValue = (e: ChangeEvent<HTMLSelectElement>) => {
+ setValue(e.target.value);
+ };
+
+ /**
+ * Get the option elements.
+ * @returns {JSX.Element[]} An array of HTML option elements.
+ */
+ const getOptions = (): JSX.Element[] =>
+ options.map((option) => (
+ <option key={option.id} value={option.value}>
+ {option.name}
+ </option>
+ ));
+
+ return (
+ <select
+ className={`${styles.field} ${styles['field--select']} ${className}`}
+ onChange={updateValue}
+ {...props}
+ >
+ {getOptions()}
+ </select>
+ );
+};
+
+export default Select;
diff --git a/src/components/atoms/headings/heading.module.scss b/src/components/atoms/headings/heading.module.scss
new file mode 100644
index 0000000..a420bc1
--- /dev/null
+++ b/src/components/atoms/headings/heading.module.scss
@@ -0,0 +1,69 @@
+@use "@styles/abstracts/functions" as fun;
+
+.heading {
+ color: var(--color-primary-dark);
+ font-family: var(--font-family-secondary);
+ letter-spacing: 0.01ex;
+
+ &--regular {
+ margin-bottom: 0;
+ margin-top: 0;
+ }
+
+ &--left {
+ text-align: left;
+ }
+
+ &--center {
+ width: fit-content;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ &--margin {
+ margin-top: 0;
+ margin-bottom: var(--spacing-sm);
+
+ & + & {
+ margin-top: var(--spacing-md);
+ }
+ }
+
+ &--1 {
+ font-size: var(--font-size-3xl);
+ font-weight: 500;
+ }
+
+ &--2 {
+ padding-bottom: fun.convert-px(3);
+ background: linear-gradient(
+ to top,
+ var(--color-primary-dark) 0.3rem,
+ transparent 0.3rem
+ )
+ 0 0 / 3rem 100% no-repeat;
+ font-size: var(--font-size-2xl);
+ font-weight: 500;
+ text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
+ }
+
+ &--3 {
+ font-size: var(--font-size-xl);
+ font-weight: 500;
+ }
+
+ &--4 {
+ font-size: var(--font-size-lg);
+ font-weight: 500;
+ }
+
+ &--5 {
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ }
+
+ &--6 {
+ font-size: var(--font-size-md);
+ font-weight: 500;
+ }
+}
diff --git a/src/components/atoms/headings/heading.stories.tsx b/src/components/atoms/headings/heading.stories.tsx
new file mode 100644
index 0000000..0e3885d
--- /dev/null
+++ b/src/components/atoms/headings/heading.stories.tsx
@@ -0,0 +1,160 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Heading from './heading';
+
+/**
+ * Heading - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Typography/Headings',
+ component: Heading,
+ args: {
+ alignment: 'left',
+ isFake: false,
+ withMargin: true,
+ },
+ argTypes: {
+ alignment: {
+ control: {
+ type: 'select',
+ },
+ description: 'The title alignment.',
+ options: ['center', 'left'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'left' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ children: {
+ description: 'Heading body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'An unique id.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isFake: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Use an heading element or only its styles.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ level: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'Heading level.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ withMargin: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Adds margin.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: true },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Heading>;
+
+const Template: ComponentStory<typeof Heading> = (args) => (
+ <Heading {...args} />
+);
+
+/**
+ * Heading Story - h1
+ */
+export const H1 = Template.bind({});
+H1.args = {
+ children: 'Your title',
+ level: 1,
+};
+
+/**
+ * Heading Story - h2
+ */
+export const H2 = Template.bind({});
+H2.args = {
+ children: 'Your title',
+ level: 2,
+};
+
+/**
+ * Heading Story - h3
+ */
+export const H3 = Template.bind({});
+H3.args = {
+ children: 'Your title',
+ level: 3,
+};
+
+/**
+ * Heading Story - h4
+ */
+export const H4 = Template.bind({});
+H4.args = {
+ children: 'Your title',
+ level: 4,
+};
+
+/**
+ * Heading Story - h5
+ */
+export const H5 = Template.bind({});
+H5.args = {
+ children: 'Your title',
+ level: 5,
+};
+
+/**
+ * Heading Story - h6
+ */
+export const H6 = Template.bind({});
+H6.args = {
+ children: 'Your title',
+ level: 6,
+};
diff --git a/src/components/atoms/headings/heading.test.tsx b/src/components/atoms/headings/heading.test.tsx
new file mode 100644
index 0000000..6b6789a
--- /dev/null
+++ b/src/components/atoms/headings/heading.test.tsx
@@ -0,0 +1,56 @@
+import { render, screen } from '@test-utils';
+import Heading from './heading';
+
+describe('Heading', () => {
+ it('renders a h1', () => {
+ render(<Heading level={1}>Level 1</Heading>);
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(
+ 'Level 1'
+ );
+ });
+
+ it('renders a h2', () => {
+ render(<Heading level={2}>Level 2</Heading>);
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
+ 'Level 2'
+ );
+ });
+
+ it('renders a h3', () => {
+ render(<Heading level={3}>Level 3</Heading>);
+ expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
+ 'Level 3'
+ );
+ });
+
+ it('renders a h4', () => {
+ render(<Heading level={4}>Level 4</Heading>);
+ expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent(
+ 'Level 4'
+ );
+ });
+
+ it('renders a h5', () => {
+ render(<Heading level={5}>Level 5</Heading>);
+ expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(
+ 'Level 5'
+ );
+ });
+
+ it('renders a h6', () => {
+ render(<Heading level={6}>Level 6</Heading>);
+ expect(screen.getByRole('heading', { level: 6 })).toHaveTextContent(
+ 'Level 6'
+ );
+ });
+
+ it('renders a text with heading styles', () => {
+ render(
+ <Heading isFake={true} level={2}>
+ Fake heading
+ </Heading>
+ );
+ expect(screen.queryByRole('heading', { level: 2 })).not.toBeInTheDocument();
+ expect(screen.getByText('Fake heading')).toHaveClass('heading');
+ });
+});
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
new file mode 100644
index 0000000..e385249
--- /dev/null
+++ b/src/components/atoms/headings/heading.tsx
@@ -0,0 +1,94 @@
+import {
+ createElement,
+ ForwardedRef,
+ forwardRef,
+ ForwardRefRenderFunction,
+ ReactNode,
+} from 'react';
+import styles from './heading.module.scss';
+
+export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
+
+export type HeadingProps = {
+ /**
+ * The title alignment. Default: left;
+ */
+ alignment?: 'center' | 'left';
+ /**
+ * The heading body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames.
+ */
+ className?: string;
+ /**
+ * The heading id.
+ */
+ id?: string;
+ /**
+ * Use an heading element or only its styles. Default: false.
+ */
+ isFake?: boolean;
+ /**
+ * HTML heading level.
+ */
+ level: HeadingLevel;
+ /**
+ * Adds margin. Default: true.
+ */
+ withMargin?: boolean;
+};
+
+type TitleTagProps = Pick<HeadingProps, 'children' | 'className' | 'id'> & {
+ tagName: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
+};
+
+const TitleTag = forwardRef<
+ HTMLHeadingElement | HTMLParagraphElement,
+ TitleTagProps
+>(
+ (
+ { children, tagName, ...props },
+ ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
+ ) => {
+ return createElement(tagName, { ...props, ref }, children);
+ }
+);
+TitleTag.displayName = 'TitleTag';
+
+/**
+ * Heading component.
+ *
+ * Render an HTML heading element or a paragraph with heading styles.
+ */
+const Heading: ForwardRefRenderFunction<HTMLDivElement, HeadingProps> = (
+ {
+ alignment = 'left',
+ children,
+ className,
+ id,
+ isFake = false,
+ level,
+ withMargin = true,
+ },
+ ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
+) => {
+ const tagName = isFake ? 'p' : (`h${level}` as TitleTagProps['tagName']);
+ const levelClass = `heading--${level}`;
+ const alignmentClass = `heading--${alignment}`;
+ const marginClass = withMargin ? 'heading--margin' : 'heading--regular';
+
+ return (
+ <TitleTag
+ tagName={tagName}
+ className={`${styles.heading} ${styles[levelClass]} ${styles[alignmentClass]} ${styles[marginClass]} ${className}`}
+ id={id}
+ ref={ref}
+ >
+ {children}
+ </TitleTag>
+ );
+};
+
+export default forwardRef(Heading);
diff --git a/src/components/atoms/icons/arrow.module.scss b/src/components/atoms/icons/arrow.module.scss
new file mode 100644
index 0000000..76e6aea
--- /dev/null
+++ b/src/components/atoms/icons/arrow.module.scss
@@ -0,0 +1,16 @@
+@use "@styles/abstracts/functions" as fun;
+
+.icon {
+ fill: var(--color-primary);
+ transition: all 0.25s ease-in-out 0s;
+
+ &--left,
+ &--right {
+ width: var(--icon-size, #{fun.convert-px(30)});
+ }
+
+ &--bottom,
+ &--top {
+ height: var(--icon-size, #{fun.convert-px(30)});
+ }
+}
diff --git a/src/components/atoms/icons/arrow.stories.tsx b/src/components/atoms/icons/arrow.stories.tsx
new file mode 100644
index 0000000..1941479
--- /dev/null
+++ b/src/components/atoms/icons/arrow.stories.tsx
@@ -0,0 +1,48 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ArrowIcon from './arrow';
+
+/**
+ * Arrow icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: ArrowIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ direction: {
+ control: {
+ type: 'select',
+ },
+ description: 'An arrow icon.',
+ options: ['bottom', 'left', 'right', 'top'],
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ArrowIcon>;
+
+const Template: ComponentStory<typeof ArrowIcon> = (args) => (
+ <ArrowIcon {...args} />
+);
+
+/**
+ * Icons Stories - Arrow
+ */
+export const Arrow = Template.bind({});
+Arrow.args = {
+ direction: 'right',
+};
diff --git a/src/components/atoms/icons/arrow.test.tsx b/src/components/atoms/icons/arrow.test.tsx
new file mode 100644
index 0000000..502dcc1
--- /dev/null
+++ b/src/components/atoms/icons/arrow.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Arrow from './arrow';
+
+describe('Arrow', () => {
+ it('renders an arrow icon oriented to the right', () => {
+ const { container } = render(<Arrow direction="right" />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Arrow/Arrow.tsx b/src/components/atoms/icons/arrow.tsx
index e9131d1..2962530 100644
--- a/src/components/Icons/Arrow/Arrow.tsx
+++ b/src/components/atoms/icons/arrow.tsx
@@ -1,12 +1,32 @@
-import styles from './Arrow.module.scss';
+import { FC } from 'react';
+import styles from './arrow.module.scss';
-type ArrowDirection = 'top' | 'right' | 'bottom' | 'left';
+export type ArrowDirection = 'top' | 'right' | 'bottom' | 'left';
+
+export type ArrowProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+ /**
+ * The arrow direction. Default: right.
+ */
+ direction: ArrowDirection;
+};
+
+/**
+ * Arrow component
+ *
+ * Render a svg arrow icon.
+ */
+const Arrow: FC<ArrowProps> = ({ className = '', direction }) => {
+ const directionClass = styles[`icon--${direction}`];
+ const classes = `${styles.icon} ${directionClass} ${className}`;
-const ArrowIcon = ({ direction = 'right' }: { direction?: ArrowDirection }) => {
if (direction === 'top') {
return (
<svg
- className={styles.icon}
+ className={classes}
viewBox="0 0 23.476 64.644995"
xmlns="http://www.w3.org/2000/svg"
>
@@ -25,7 +45,7 @@ const ArrowIcon = ({ direction = 'right' }: { direction?: ArrowDirection }) => {
if (direction === 'bottom') {
return (
<svg
- className={styles.icon}
+ className={classes}
viewBox="0 0 23.476 64.644995"
xmlns="http://www.w3.org/2000/svg"
>
@@ -44,7 +64,7 @@ const ArrowIcon = ({ direction = 'right' }: { direction?: ArrowDirection }) => {
if (direction === 'left') {
return (
<svg
- className={styles.icon}
+ className={classes}
viewBox="0 0 64.644997 23.476001"
xmlns="http://www.w3.org/2000/svg"
>
@@ -62,7 +82,7 @@ const ArrowIcon = ({ direction = 'right' }: { direction?: ArrowDirection }) => {
return (
<svg
- className={styles.icon}
+ className={classes}
viewBox="0 0 64.644997 23.476001"
xmlns="http://www.w3.org/2000/svg"
>
@@ -78,4 +98,4 @@ const ArrowIcon = ({ direction = 'right' }: { direction?: ArrowDirection }) => {
);
};
-export default ArrowIcon;
+export default Arrow;
diff --git a/src/components/Icons/CV/CV.module.scss b/src/components/atoms/icons/career.module.scss
index aaa8a1a..c5d65eb 100644
--- a/src/components/Icons/CV/CV.module.scss
+++ b/src/components/atoms/icons/career.module.scss
@@ -2,7 +2,6 @@
.icon {
display: block;
- margin: auto;
width: var(--icon-size, #{fun.convert-px(40)});
}
diff --git a/src/components/atoms/icons/career.stories.tsx b/src/components/atoms/icons/career.stories.tsx
new file mode 100644
index 0000000..7b11bb8
--- /dev/null
+++ b/src/components/atoms/icons/career.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CareerIcon from './career';
+
+/**
+ * Career icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: CareerIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CareerIcon>;
+
+const Template: ComponentStory<typeof CareerIcon> = (args) => (
+ <CareerIcon {...args} />
+);
+
+/**
+ * Icons Stories - Career
+ */
+export const Career = Template.bind({});
diff --git a/src/components/atoms/icons/career.test.tsx b/src/components/atoms/icons/career.test.tsx
new file mode 100644
index 0000000..62ffc14
--- /dev/null
+++ b/src/components/atoms/icons/career.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Career from './career';
+
+describe('Career', () => {
+ it('renders a Career icon', () => {
+ const { container } = render(<Career />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/CV/CV.tsx b/src/components/atoms/icons/career.tsx
index 876d1cb..f28d399 100644
--- a/src/components/Icons/CV/CV.tsx
+++ b/src/components/atoms/icons/career.tsx
@@ -1,11 +1,24 @@
-import styles from './CV.module.scss';
+import { FC } from 'react';
+import styles from './career.module.scss';
-const CVIcon = () => {
+export type CareerProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * Career Component
+ *
+ * Render a career svg icon.
+ */
+const Career: FC<CareerProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path
className={styles.bottom}
@@ -55,4 +68,4 @@ const CVIcon = () => {
);
};
-export default CVIcon;
+export default Career;
diff --git a/src/components/atoms/icons/cc-by-sa.module.scss b/src/components/atoms/icons/cc-by-sa.module.scss
new file mode 100644
index 0000000..e1b2100
--- /dev/null
+++ b/src/components/atoms/icons/cc-by-sa.module.scss
@@ -0,0 +1,7 @@
+@use "@styles/abstracts/functions" as fun;
+
+.icon {
+ display: block;
+ width: var(--icon-size, #{fun.convert-px(60)});
+ fill: var(--color-fg);
+}
diff --git a/src/components/atoms/icons/cc-by-sa.stories.tsx b/src/components/atoms/icons/cc-by-sa.stories.tsx
new file mode 100644
index 0000000..4229725
--- /dev/null
+++ b/src/components/atoms/icons/cc-by-sa.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CCBySAIcon from './cc-by-sa';
+
+/**
+ * CC BY SA icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: CCBySAIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CCBySAIcon>;
+
+const Template: ComponentStory<typeof CCBySAIcon> = (args) => (
+ <CCBySAIcon {...args} />
+);
+
+/**
+ * Icons Stories - CC BY SA
+ */
+export const CCBySA = Template.bind({});
diff --git a/src/components/atoms/icons/cc-by-sa.test.tsx b/src/components/atoms/icons/cc-by-sa.test.tsx
new file mode 100644
index 0000000..adb03e4
--- /dev/null
+++ b/src/components/atoms/icons/cc-by-sa.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import CCBySA from './cc-by-sa';
+
+describe('CCBySA', () => {
+ it('renders a CC BY SA icon', () => {
+ render(<CCBySA />);
+ expect(screen.getByTitle('CC BY SA')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/Icons/Copyright/Copyright.tsx b/src/components/atoms/icons/cc-by-sa.tsx
index d27c042..8239154 100644
--- a/src/components/Icons/Copyright/Copyright.tsx
+++ b/src/components/atoms/icons/cc-by-sa.tsx
@@ -1,13 +1,35 @@
-import styles from './Copyright.module.scss';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './cc-by-sa.module.scss';
+
+export type CCBySAProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * CCBySA component
+ *
+ * Render a CC BY SA svg icon.
+ */
+const CCBySA: FC<CCBySAProps> = ({ className = '' }) => {
+ const intl = useIntl();
-const CopyrightIcon = () => {
return (
<svg
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
viewBox="0 0 211.99811 63.999996"
xmlns="http://www.w3.org/2000/svg"
>
- <title>CC BY SA</title>
+ <title>
+ {intl.formatMessage({
+ defaultMessage: 'CC BY SA',
+ description: 'CCBySA: icon title',
+ id: 'cl7YNU',
+ })}
+ </title>
<path d="m 175.53911,15.829498 c 0,-3.008 1.485,-4.514 4.458,-4.514 2.973,0 4.457,1.504 4.457,4.514 0,2.971 -1.486,4.457 -4.457,4.457 -2.971,0 -4.458,-1.486 -4.458,-4.457 z" />
<path d="m 188.62611,24.057498 v 13.085 h -3.656 v 15.542 h -9.944 v -15.541 h -3.656 v -13.086 c 0,-0.572 0.2,-1.057 0.599,-1.457 0.401,-0.399 0.887,-0.6 1.457,-0.6 h 13.144 c 0.533,0 1.01,0.2 1.428,0.6 0.417,0.4 0.628,0.886 0.628,1.457 z" />
<path d="m 179.94147,-1.9073486e-6 c -8.839,0 -16.34167,3.0848125073486 -22.51367,9.2578125073486 -6.285,6.4000004 -9.42969,13.9811874 -9.42969,22.7421874 0,8.762 3.14469,16.284312 9.42969,22.570312 6.361,6.286 13.86467,9.429688 22.51367,9.429688 8.799,0 16.43611,-3.181922 22.91211,-9.544922 6.096,-5.98 9.14453,-13.464078 9.14453,-22.455078 0,-8.952 -3.10646,-16.532188 -9.31446,-22.7421874 -6.172,-6.172 -13.75418,-9.2578125073486 -22.74218,-9.2578125073486 z M 180.05475,5.7714825 c 7.238,0 13.40967,2.55225 18.51367,7.6562495 5.103,5.106 7.65625,11.294313 7.65625,18.570313 0,7.391 -2.51397,13.50575 -7.54297,18.34375 -5.295,5.221 -11.50591,7.828125 -18.6289,7.828125 -7.162,0 -13.33268,-2.589484 -18.51368,-7.771484 -5.18,-5.178001 -7.76953,-11.310485 -7.76953,-18.396485 0,-7.047 2.60813,-13.238266 7.82813,-18.572265 5.029,-5.1040004 11.18103,-7.6582035 18.45703,-7.6582035 z" />
@@ -20,4 +42,4 @@ const CopyrightIcon = () => {
);
};
-export default CopyrightIcon;
+export default CCBySA;
diff --git a/src/components/Icons/Close/Close.module.scss b/src/components/atoms/icons/close.module.scss
index 5a1f638..4a5d18d 100644
--- a/src/components/Icons/Close/Close.module.scss
+++ b/src/components/atoms/icons/close.module.scss
@@ -2,7 +2,6 @@
.icon {
display: block;
- margin: auto;
width: var(--icon-size, #{fun.convert-px(40)});
}
diff --git a/src/components/atoms/icons/close.stories.tsx b/src/components/atoms/icons/close.stories.tsx
new file mode 100644
index 0000000..f9628db
--- /dev/null
+++ b/src/components/atoms/icons/close.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CloseIcon from './close';
+
+/**
+ * Close icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: CloseIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CloseIcon>;
+
+const Template: ComponentStory<typeof CloseIcon> = (args) => (
+ <CloseIcon {...args} />
+);
+
+/**
+ * Icons Stories - Close
+ */
+export const Close = Template.bind({});
diff --git a/src/components/atoms/icons/close.test.tsx b/src/components/atoms/icons/close.test.tsx
new file mode 100644
index 0000000..0357bec
--- /dev/null
+++ b/src/components/atoms/icons/close.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Close from './close';
+
+describe('Close', () => {
+ it('renders a Close icon', () => {
+ const { container } = render(<Close />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Close/Close.tsx b/src/components/atoms/icons/close.tsx
index 12214de..3e0adb5 100644
--- a/src/components/Icons/Close/Close.tsx
+++ b/src/components/atoms/icons/close.tsx
@@ -1,11 +1,24 @@
-import styles from './Close.module.scss';
+import { FC } from 'react';
+import styles from './close.module.scss';
-const CloseIcon = () => {
+export type CloseProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * Close component
+ *
+ * Render a close svg icon.
+ */
+const Close: FC<CloseProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path
className={styles.line}
@@ -19,4 +32,4 @@ const CloseIcon = () => {
);
};
-export default CloseIcon;
+export default Close;
diff --git a/src/components/Icons/Cog/Cog.module.scss b/src/components/atoms/icons/cog.module.scss
index a861f0c..5201598 100644
--- a/src/components/Icons/Cog/Cog.module.scss
+++ b/src/components/atoms/icons/cog.module.scss
@@ -1,9 +1,7 @@
@use "@styles/abstracts/functions" as fun;
.icon {
- display: block;
width: var(--icon-size, #{fun.convert-px(40)});
- margin: auto;
fill: var(--color-primary-lighter);
stroke: var(--color-primary-darker);
stroke-width: 4;
diff --git a/src/components/atoms/icons/cog.stories.tsx b/src/components/atoms/icons/cog.stories.tsx
new file mode 100644
index 0000000..631f30d
--- /dev/null
+++ b/src/components/atoms/icons/cog.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CogIcon from './cog';
+
+/**
+ * Cogs icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: CogIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CogIcon>;
+
+const Template: ComponentStory<typeof CogIcon> = (args) => (
+ <CogIcon {...args} />
+);
+
+/**
+ * Icons Stories - Cogs
+ */
+export const Cog = Template.bind({});
diff --git a/src/components/atoms/icons/cog.test.tsx b/src/components/atoms/icons/cog.test.tsx
new file mode 100644
index 0000000..89090fa
--- /dev/null
+++ b/src/components/atoms/icons/cog.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Cog from './cog';
+
+describe('Cog', () => {
+ it('renders a Cog icon', () => {
+ const { container } = render(<Cog />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Cog/Cog.tsx b/src/components/atoms/icons/cog.tsx
index 7a04d76..9e78a7b 100644
--- a/src/components/Icons/Cog/Cog.tsx
+++ b/src/components/atoms/icons/cog.tsx
@@ -1,11 +1,24 @@
-import styles from './Cog.module.scss';
+import { FC } from 'react';
+import styles from './cog.module.scss';
-const CogIcon = () => {
+export type CogProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * Cog component
+ *
+ * Render a cog svg icon.
+ */
+const Cog: FC<CogProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path d="m 71.782287,3.1230469 c -1.164356,0 -2.3107,0.076326 -3.435131,0.2227895 L 66.33766,9.1021499 C 64.651951,9.5517047 63.049493,10.204637 61.558109,11.033725 L 56.112383,8.2889128 c -1.970928,1.4609237 -3.730521,3.1910632 -5.22513,5.1351362 l 2.648234,5.494014 c -0.855644,1.477262 -1.537042,3.067161 -2.016082,4.743334 l -5.791433,1.911821 c -0.188001,1.269731 -0.286444,2.568579 -0.286444,3.890587 0,1.164355 0.07633,2.310701 0.222789,3.435131 l 5.756315,2.009497 c 0.449555,1.685708 1.102486,3.288168 1.931575,4.779551 l -2.744813,5.445725 c 1.460924,1.970927 3.191063,3.730521 5.135137,5.22513 l 5.494014,-2.648233 c 1.477261,0.85564 3.067161,1.537039 4.743334,2.016081 L 67.8917,55.51812 c 1.26973,0.188002 2.568578,0.286444 3.890587,0.286444 1.16565,0 2.313889,-0.07601 3.43952,-0.222789 l 2.008399,-5.756314 c 1.684332,-0.449523 3.285984,-1.103103 4.776259,-1.931575 l 5.445725,2.744812 c 1.970928,-1.460924 3.730521,-3.191061 5.22513,-5.135136 l -2.648233,-5.494015 c 0.85564,-1.477262 1.537039,-3.067161 2.016082,-4.743334 l 5.79253,-1.91182 c 0.187995,-1.269731 0.285346,-2.56858 0.285346,-3.890588 0,-1.16565 -0.07601,-2.313889 -0.222789,-3.439521 L 92.143942,24.015886 C 91.694419,22.331554 91.04084,20.729903 90.212367,19.239628 l 2.744812,-5.445726 C 91.496255,11.822973 89.766118,10.063381 87.822043,8.5687715 L 82.328028,11.217006 C 80.850766,10.361361 79.260867,9.6799641 77.584694,9.2009234 L 75.672874,3.4094907 C 74.403143,3.2214898 73.104295,3.1230469 71.782287,3.1230469 Z m 0,15.0520191 a 11.288679,11.288679 0 0 1 11.288739,11.288739 11.288679,11.288679 0 0 1 -11.288739,11.28874 11.288679,11.288679 0 0 1 -11.28874,-11.28874 11.288679,11.288679 0 0 1 11.28874,-11.288739 z" />
<path d="m 38.326115,25.84777 c -1.583642,0 -3.142788,0.103807 -4.672127,0.303016 l -2.73312,7.829173 c -2.292736,0.611441 -4.472242,1.499494 -6.500676,2.627139 L 17.01345,32.873874 c -2.680664,1.987004 -5.073889,4.340169 -7.1067095,6.984309 l 3.6018685,7.472418 c -1.163764,2.009226 -2.090533,4.171652 -2.742078,6.451418 l -7.8769382,2.60027 C 2.6338924,58.109252 2.5,59.875819 2.5,61.673885 c 0,1.583642 0.1038125,3.142788 0.3030165,4.672128 l 7.8291725,2.73312 c 0.611441,2.292734 1.499494,4.472243 2.627139,6.500673 L 9.5261037,82.98655 c 1.9870063,2.680661 4.3401703,5.07389 6.9843093,7.106709 l 7.472419,-3.601867 c 2.009226,1.16376 4.171651,2.090533 6.451418,2.742079 l 2.60027,7.876932 C 34.761483,97.366114 36.528049,97.5 38.326115,97.5 c 1.585404,0 3.147126,-0.103373 4.678099,-0.303015 l 2.731628,-7.829178 c 2.290862,-0.611397 4.469272,-1.500329 6.496197,-2.627132 l 7.406741,3.733224 c 2.680664,-1.987007 5.07389,-4.340171 7.10671,-6.984313 l -3.601866,-7.472415 c 1.163756,-2.00923 2.090529,-4.171655 2.742076,-6.45142 l 7.878431,-2.60027 c 0.255691,-1.726964 0.3881,-3.49353 0.3881,-5.291596 0,-1.585404 -0.103373,-3.147127 -0.303016,-4.678099 L 66.020041,54.264159 C 65.408645,51.973296 64.51971,49.794888 63.392903,47.767962 l 3.733224,-7.406742 c -1.987006,-2.680664 -4.340168,-5.073889 -6.984309,-7.10671 l -7.472419,3.601867 c -2.009228,-1.163762 -4.171651,-2.090533 -6.451418,-2.742076 l -2.60027,-7.876939 C 41.890748,25.981661 40.124181,25.84777 38.326115,25.84777 Z m 0,20.472278 A 15.353754,15.353754 0 0 1 53.679952,61.673885 15.353754,15.353754 0 0 1 38.326115,77.027724 15.353754,15.353754 0 0 1 22.972279,61.673885 15.353754,15.353754 0 0 1 38.326115,46.320048 Z" />
@@ -13,4 +26,4 @@ const CogIcon = () => {
);
};
-export default CogIcon;
+export default Cog;
diff --git a/src/components/Icons/Projects/Projects.module.scss b/src/components/atoms/icons/computer-screen.module.scss
index 3cf939a..6c8f701 100644
--- a/src/components/Icons/Projects/Projects.module.scss
+++ b/src/components/atoms/icons/computer-screen.module.scss
@@ -2,7 +2,6 @@
.icon {
display: block;
- margin: auto;
width: var(--icon-size, #{fun.convert-px(40)});
}
diff --git a/src/components/atoms/icons/computer-screen.stories.tsx b/src/components/atoms/icons/computer-screen.stories.tsx
new file mode 100644
index 0000000..19649ad
--- /dev/null
+++ b/src/components/atoms/icons/computer-screen.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ComputerScreenIcon from './computer-screen';
+
+/**
+ * Computer Screen icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: ComputerScreenIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ComputerScreenIcon>;
+
+const Template: ComponentStory<typeof ComputerScreenIcon> = (args) => (
+ <ComputerScreenIcon {...args} />
+);
+
+/**
+ * Icons Stories - Computer Screen
+ */
+export const ComputerScreen = Template.bind({});
diff --git a/src/components/atoms/icons/computer-screen.test.tsx b/src/components/atoms/icons/computer-screen.test.tsx
new file mode 100644
index 0000000..c0e53e0
--- /dev/null
+++ b/src/components/atoms/icons/computer-screen.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import ComputerScreen from './computer-screen';
+
+describe('ComputerScreen', () => {
+ it('renders a computer screen icon', () => {
+ const { container } = render(<ComputerScreen />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Projects/Projects.tsx b/src/components/atoms/icons/computer-screen.tsx
index d4af247..8786139 100644
--- a/src/components/Icons/Projects/Projects.tsx
+++ b/src/components/atoms/icons/computer-screen.tsx
@@ -1,11 +1,24 @@
-import styles from './Projects.module.scss';
+import { FC } from 'react';
+import styles from './computer-screen.module.scss';
-const ProjectsIcon = () => {
+export type ComputerScreenProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * ComputerScreen component
+ *
+ * Render a computer screen svg icon.
+ */
+const ComputerScreen: FC<ComputerScreenProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path
d="M 1.0206528,11.991149 H 98.979347 V 78.466748 H 1.0206528 Z"
@@ -63,4 +76,4 @@ const ProjectsIcon = () => {
);
};
-export default ProjectsIcon;
+export default ComputerScreen;
diff --git a/src/components/Icons/Contact/Contact.module.scss b/src/components/atoms/icons/envelop.module.scss
index 963c1dc..202900b 100644
--- a/src/components/Icons/Contact/Contact.module.scss
+++ b/src/components/atoms/icons/envelop.module.scss
@@ -2,7 +2,6 @@
.icon {
display: block;
- margin: auto;
width: var(--icon-size, #{fun.convert-px(40)});
}
diff --git a/src/components/atoms/icons/envelop.stories.tsx b/src/components/atoms/icons/envelop.stories.tsx
new file mode 100644
index 0000000..efa94dd
--- /dev/null
+++ b/src/components/atoms/icons/envelop.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import EnvelopIcon from './envelop';
+
+/**
+ * Envelop icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: EnvelopIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof EnvelopIcon>;
+
+const Template: ComponentStory<typeof EnvelopIcon> = (args) => (
+ <EnvelopIcon {...args} />
+);
+
+/**
+ * Icons Stories - Envelop
+ */
+export const Envelop = Template.bind({});
diff --git a/src/components/atoms/icons/envelop.test.tsx b/src/components/atoms/icons/envelop.test.tsx
new file mode 100644
index 0000000..072dc85
--- /dev/null
+++ b/src/components/atoms/icons/envelop.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Envelop from './envelop';
+
+describe('Envelop', () => {
+ it('renders an envelop icon', () => {
+ const { container } = render(<Envelop />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Contact/Contact.tsx b/src/components/atoms/icons/envelop.tsx
index 19295d0..84dca97 100644
--- a/src/components/Icons/Contact/Contact.tsx
+++ b/src/components/atoms/icons/envelop.tsx
@@ -1,11 +1,24 @@
-import styles from './Contact.module.scss';
+import { FC } from 'react';
+import styles from './envelop.module.scss';
-const ContactIcon = () => {
+export type EnvelopProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * Envelop Component
+ *
+ * Render an envelop svg icon.
+ */
+const Envelop: FC<EnvelopProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path
className={styles.background}
@@ -51,4 +64,4 @@ const ContactIcon = () => {
);
};
-export default ContactIcon;
+export default Envelop;
diff --git a/src/components/Icons/Copyright/Copyright.module.scss b/src/components/atoms/icons/feed.module.scss
index 8ea801e..56a5253 100644
--- a/src/components/Icons/Copyright/Copyright.module.scss
+++ b/src/components/atoms/icons/feed.module.scss
@@ -3,5 +3,4 @@
.icon {
display: block;
width: var(--icon-size, #{fun.convert-px(40)});
- fill: var(--color-fg);
}
diff --git a/src/components/atoms/icons/feed.stories.tsx b/src/components/atoms/icons/feed.stories.tsx
new file mode 100644
index 0000000..e3587a8
--- /dev/null
+++ b/src/components/atoms/icons/feed.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import FeedIcon from './feed';
+
+/**
+ * Feed icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: FeedIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof FeedIcon>;
+
+const Template: ComponentStory<typeof FeedIcon> = (args) => (
+ <FeedIcon {...args} />
+);
+
+/**
+ * Icons Stories - Feed
+ */
+export const Feed = Template.bind({});
diff --git a/src/components/atoms/icons/feed.test.tsx b/src/components/atoms/icons/feed.test.tsx
new file mode 100644
index 0000000..fed9da9
--- /dev/null
+++ b/src/components/atoms/icons/feed.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Feed from './feed';
+
+describe('Feed', () => {
+ it('renders a feed icon', () => {
+ const { container } = render(<Feed />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/atoms/icons/feed.tsx b/src/components/atoms/icons/feed.tsx
new file mode 100644
index 0000000..6839abd
--- /dev/null
+++ b/src/components/atoms/icons/feed.tsx
@@ -0,0 +1,74 @@
+import { FC } from 'react';
+import styles from './feed.module.scss';
+
+export type FeedProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * Feed Component
+ *
+ * Render a feed svg icon.
+ */
+const Feed: FC<FeedProps> = ({ className = '' }) => {
+ return (
+ <svg
+ viewBox="0 0 256 256"
+ xmlns="http://www.w3.org/2000/svg"
+ className={`${styles.icon} ${className}`}
+ >
+ <defs>
+ <linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
+ <stop offset="0.0" stopColor="#E3702D" />
+ <stop offset="0.1071" stopColor="#EA7D31" />
+ <stop offset="0.3503" stopColor="#F69537" />
+ <stop offset="0.5" stopColor="#FB9E3A" />
+ <stop offset="0.7016" stopColor="#EA7C31" />
+ <stop offset="0.8866" stopColor="#DE642B" />
+ <stop offset="1.0" stopColor="#D95B29" />
+ </linearGradient>
+ </defs>
+ <rect
+ width="256"
+ height="256"
+ rx="55"
+ ry="55"
+ x="0"
+ y="0"
+ fill="#CC5D15"
+ />
+ <rect
+ width="246"
+ height="246"
+ rx="50"
+ ry="50"
+ x="5"
+ y="5"
+ fill="#F49C52"
+ />
+ <rect
+ width="236"
+ height="236"
+ rx="47"
+ ry="47"
+ x="10"
+ y="10"
+ fill="url(#RSSg)"
+ />
+ <circle cx="68" cy="189" r="24" fill="#FFF" />
+ <path
+ d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z"
+ fill="#FFF"
+ />
+ <path
+ d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z"
+ fill="#FFF"
+ />
+ </svg>
+ );
+};
+
+export default Feed;
diff --git a/src/components/Icons/Hamburger/Hamburger.module.scss b/src/components/atoms/icons/hamburger.module.scss
index 9965c5e..4fba4df 100644
--- a/src/components/Icons/Hamburger/Hamburger.module.scss
+++ b/src/components/atoms/icons/hamburger.module.scss
@@ -1,12 +1,21 @@
@use "@styles/abstracts/functions" as fun;
-.icon {
+.wrapper {
+ display: flex;
+ align-items: center;
+ width: var(--icon-size, #{fun.convert-px(50)});
+ height: var(--icon-size, #{fun.convert-px(50)});
position: relative;
- width: 100%;
+}
+.icon {
&,
&::before,
&::after {
+ display: block;
+ height: fun.convert-px(7);
+ width: 100%;
+ position: absolute;
background: var(--color-primary-lighter);
background-image: linear-gradient(
to right,
@@ -15,42 +24,19 @@
);
border: fun.convert-px(1) solid var(--color-primary-darker);
border-radius: fun.convert-px(3);
- display: block;
- height: fun.convert-px(7);
- margin: auto;
transition: all 0.25s ease-in-out 0s, transform 0.4s ease-in 0s;
}
&::before,
&::after {
content: "";
- position: absolute;
- left: fun.convert-px(-1);
- right: fun.convert-px(-1);
}
&::before {
- bottom: fun.convert-px(15);
+ top: fun.convert-px(-15);
}
&::after {
- top: fun.convert-px(15);
- }
-
- &--active {
- background: transparent;
- border: transparent;
-
- &::before {
- transform-origin: 50% 50%;
- transform: rotate(45deg);
- bottom: 0;
- }
-
- &::after {
- transform-origin: 50% 50%;
- transform: rotate(-45deg);
- top: 0;
- }
+ bottom: fun.convert-px(-15);
}
}
diff --git a/src/components/atoms/icons/hamburger.stories.tsx b/src/components/atoms/icons/hamburger.stories.tsx
new file mode 100644
index 0000000..0a8a8cc
--- /dev/null
+++ b/src/components/atoms/icons/hamburger.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import HamburgerIcon from './hamburger';
+
+/**
+ * Hamburger icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: HamburgerIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the icon wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ iconClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the icon.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof HamburgerIcon>;
+
+const Template: ComponentStory<typeof HamburgerIcon> = (args) => (
+ <HamburgerIcon {...args} />
+);
+
+/**
+ * Icons Stories - Hamburger
+ */
+export const Hamburger = Template.bind({});
diff --git a/src/components/atoms/icons/hamburger.test.tsx b/src/components/atoms/icons/hamburger.test.tsx
new file mode 100644
index 0000000..7173a23
--- /dev/null
+++ b/src/components/atoms/icons/hamburger.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Hamburger from './hamburger';
+
+describe('Hamburger', () => {
+ it('renders a Hamburger icon', () => {
+ const { container } = render(<Hamburger />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/atoms/icons/hamburger.tsx b/src/components/atoms/icons/hamburger.tsx
new file mode 100644
index 0000000..93aed2a
--- /dev/null
+++ b/src/components/atoms/icons/hamburger.tsx
@@ -0,0 +1,32 @@
+import { FC } from 'react';
+import styles from './hamburger.module.scss';
+
+export type HamburgerProps = {
+ /**
+ * Set additional classnames to the icon wrapper.
+ */
+ className?: string;
+
+ /**
+ * Set additional classnames to the icon.
+ */
+ iconClassName?: string;
+};
+
+/**
+ * Hamburger component
+ *
+ * Render a Hamburger icon.
+ */
+const Hamburger: FC<HamburgerProps> = ({
+ className = '',
+ iconClassName = '',
+}) => {
+ return (
+ <span className={`${styles.wrapper} ${className}`}>
+ <span className={`${styles.icon} ${iconClassName}`}></span>
+ </span>
+ );
+};
+
+export default Hamburger;
diff --git a/src/components/Icons/Home/Home.module.scss b/src/components/atoms/icons/home.module.scss
index f2e7f9e..48dcc6c 100644
--- a/src/components/Icons/Home/Home.module.scss
+++ b/src/components/atoms/icons/home.module.scss
@@ -2,7 +2,6 @@
.icon {
display: block;
- margin: auto;
width: var(--icon-size, #{fun.convert-px(40)});
}
diff --git a/src/components/atoms/icons/home.stories.tsx b/src/components/atoms/icons/home.stories.tsx
new file mode 100644
index 0000000..ffb3061
--- /dev/null
+++ b/src/components/atoms/icons/home.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import HomeIcon from './home';
+
+/**
+ * Home icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: HomeIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof HomeIcon>;
+
+const Template: ComponentStory<typeof HomeIcon> = (args) => (
+ <HomeIcon {...args} />
+);
+
+/**
+ * Icons Stories - Home
+ */
+export const Home = Template.bind({});
diff --git a/src/components/atoms/icons/home.test.tsx b/src/components/atoms/icons/home.test.tsx
new file mode 100644
index 0000000..a08a3cf
--- /dev/null
+++ b/src/components/atoms/icons/home.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Home from './home';
+
+describe('Home', () => {
+ it('renders a home icon', () => {
+ const { container } = render(<Home />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Home/Home.tsx b/src/components/atoms/icons/home.tsx
index 11c0c8c..3b6732d 100644
--- a/src/components/Icons/Home/Home.tsx
+++ b/src/components/atoms/icons/home.tsx
@@ -1,11 +1,24 @@
-import styles from './Home.module.scss';
+import { FC } from 'react';
+import styles from './home.module.scss';
-const HomeIcon = () => {
+export type HomeProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * Home component.
+ *
+ * Render a home svg icon.
+ */
+const Home: FC<HomeProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path
className={styles.wall}
@@ -39,4 +52,4 @@ const HomeIcon = () => {
);
};
-export default HomeIcon;
+export default Home;
diff --git a/src/components/Icons/Search/Search.module.scss b/src/components/atoms/icons/magnifying-glass.module.scss
index 4c42028..d14bec5 100644
--- a/src/components/Icons/Search/Search.module.scss
+++ b/src/components/atoms/icons/magnifying-glass.module.scss
@@ -1,8 +1,6 @@
@use "@styles/abstracts/functions" as fun;
.icon {
- display: block;
- margin: auto;
width: var(--icon-size, #{fun.convert-px(40)});
}
diff --git a/src/components/atoms/icons/magnifying-glass.stories.tsx b/src/components/atoms/icons/magnifying-glass.stories.tsx
new file mode 100644
index 0000000..3e33deb
--- /dev/null
+++ b/src/components/atoms/icons/magnifying-glass.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import MagnifyingGlassIcon from './magnifying-glass';
+
+/**
+ * Magnifying Glass icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: MagnifyingGlassIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof MagnifyingGlassIcon>;
+
+const Template: ComponentStory<typeof MagnifyingGlassIcon> = (args) => (
+ <MagnifyingGlassIcon {...args} />
+);
+
+/**
+ * Icons Stories - Magnifying Glass
+ */
+export const MagnifyingGlass = Template.bind({});
diff --git a/src/components/atoms/icons/magnifying-glass.test.tsx b/src/components/atoms/icons/magnifying-glass.test.tsx
new file mode 100644
index 0000000..8e788f7
--- /dev/null
+++ b/src/components/atoms/icons/magnifying-glass.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import MagnifyingGlass from './magnifying-glass';
+
+describe('MagnifyingGlass', () => {
+ it('renders a magnifying glass icon', () => {
+ const { container } = render(<MagnifyingGlass />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Search/Search.tsx b/src/components/atoms/icons/magnifying-glass.tsx
index abb7b53..1ca2a44 100644
--- a/src/components/Icons/Search/Search.tsx
+++ b/src/components/atoms/icons/magnifying-glass.tsx
@@ -1,11 +1,24 @@
-import styles from './Search.module.scss';
+import { FC } from 'react';
+import styles from './magnifying-glass.module.scss';
-const SearchIcon = () => {
+export type MagnifyingGlassProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * MagnifyingGlass component
+ *
+ * Render a magnifying glass svg icon.
+ */
+const MagnifyingGlass: FC<MagnifyingGlassProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path
className={styles['small-handle']}
@@ -27,4 +40,4 @@ const SearchIcon = () => {
);
};
-export default SearchIcon;
+export default MagnifyingGlass;
diff --git a/src/components/Icons/Moon/Moon.module.scss b/src/components/atoms/icons/moon.module.scss
index 799a282..e0b53d6 100644
--- a/src/components/Icons/Moon/Moon.module.scss
+++ b/src/components/atoms/icons/moon.module.scss
@@ -1,6 +1,6 @@
@use "@styles/abstracts/functions" as fun;
-.moon {
+.icon {
fill: var(--color-primary-lighter);
stroke: var(--color-primary-darker);
stroke-width: 4;
diff --git a/src/components/atoms/icons/moon.stories.tsx b/src/components/atoms/icons/moon.stories.tsx
new file mode 100644
index 0000000..e8b34de
--- /dev/null
+++ b/src/components/atoms/icons/moon.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import MoonIcon from './moon';
+
+/**
+ * Moon icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: MoonIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The SVG title.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof MoonIcon>;
+
+const Template: ComponentStory<typeof MoonIcon> = (args) => (
+ <MoonIcon {...args} />
+);
+
+/**
+ * Icons Stories - Moon
+ */
+export const Moon = Template.bind({});
diff --git a/src/components/atoms/icons/moon.test.tsx b/src/components/atoms/icons/moon.test.tsx
new file mode 100644
index 0000000..1c96303
--- /dev/null
+++ b/src/components/atoms/icons/moon.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Moon from './moon';
+
+describe('Moon', () => {
+ it('renders a moon icon', () => {
+ const { container } = render(<Moon />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Moon/Moon.tsx b/src/components/atoms/icons/moon.tsx
index 26f56a1..ec4fa0c 100644
--- a/src/components/Icons/Moon/Moon.tsx
+++ b/src/components/atoms/icons/moon.tsx
@@ -1,25 +1,28 @@
-import { useIntl } from 'react-intl';
-import styles from './Moon.module.scss';
+import { FC } from 'react';
+import styles from './moon.module.scss';
-const MoonIcon = () => {
- const intl = useIntl();
+export type MoonProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+ /**
+ * The SVG title.
+ */
+ title?: string;
+};
+const Moon: FC<MoonProps> = ({ className = '', title }) => {
return (
<svg
- className={styles.moon}
+ className={`${styles.icon} ${className}`}
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
- <title>
- {intl.formatMessage({
- defaultMessage: 'Dark theme',
- description: 'Icons: Moon icon (dark theme)',
- id: 'ode0YK',
- })}
- </title>
+ {title !== 'undefined' && <title>{title}</title>}
<path d="M 51.077315,1.9893942 A 43.319985,43.319985 0 0 1 72.840039,39.563145 43.319985,43.319985 0 0 1 29.520053,82.88313 43.319985,43.319985 0 0 1 5.4309911,75.569042 48.132997,48.132997 0 0 0 46.126047,98 48.132997,48.132997 0 0 0 94.260004,49.867002 48.132997,48.132997 0 0 0 51.077315,1.9893942 Z" />
</svg>
);
};
-export default MoonIcon;
+export default Moon;
diff --git a/src/components/atoms/icons/plus-minus.module.scss b/src/components/atoms/icons/plus-minus.module.scss
new file mode 100644
index 0000000..c54db33
--- /dev/null
+++ b/src/components/atoms/icons/plus-minus.module.scss
@@ -0,0 +1,39 @@
+@use "@styles/abstracts/functions" as fun;
+
+.icon {
+ display: flex;
+ place-content: center;
+ place-items: center;
+ width: var(--icon-size, #{fun.convert-px(30)});
+ height: var(--icon-size, #{fun.convert-px(30)});
+ position: relative;
+ background: var(--color-bg);
+ border: fun.convert-px(1) solid var(--color-primary);
+ border-radius: fun.convert-px(3);
+ color: var(--color-primary);
+
+ &::before,
+ &::after {
+ content: "";
+ position: absolute;
+ background: var(--color-primary);
+ transition: transform 0.4s ease-out 0s;
+ }
+
+ &::after {
+ height: fun.convert-px(3);
+ width: 60%;
+ }
+
+ &::before {
+ height: 60%;
+ width: fun.convert-px(3);
+ transform: scaleY(1);
+ }
+
+ &--minus {
+ &::before {
+ transform: scaleY(0);
+ }
+ }
+}
diff --git a/src/components/atoms/icons/plus-minus.stories.tsx b/src/components/atoms/icons/plus-minus.stories.tsx
new file mode 100644
index 0000000..eebf1e5
--- /dev/null
+++ b/src/components/atoms/icons/plus-minus.stories.tsx
@@ -0,0 +1,49 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import PlusMinusIcon from './plus-minus';
+
+/**
+ * Plus/Minus icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: PlusMinusIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ state: {
+ control: {
+ type: 'radio',
+ },
+ description: 'Which state should be displayed.',
+ options: ['plus', 'minus'],
+ type: {
+ name: 'enum',
+ required: true,
+ value: ['plus', 'minus'],
+ },
+ },
+ },
+} as ComponentMeta<typeof PlusMinusIcon>;
+
+const Template: ComponentStory<typeof PlusMinusIcon> = (args) => (
+ <PlusMinusIcon {...args} />
+);
+
+/**
+ * Icons Stories - Plus/Minus
+ */
+export const PlusMinus = Template.bind({});
+PlusMinus.args = {
+ state: 'plus',
+};
diff --git a/src/components/atoms/icons/plus-minus.test.tsx b/src/components/atoms/icons/plus-minus.test.tsx
new file mode 100644
index 0000000..6903c7a
--- /dev/null
+++ b/src/components/atoms/icons/plus-minus.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import PlusMinus from './plus-minus';
+
+describe('PlusMinus', () => {
+ it('renders a plus/minus icon', () => {
+ const { container } = render(<PlusMinus state="plus" />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/atoms/icons/plus-minus.tsx b/src/components/atoms/icons/plus-minus.tsx
new file mode 100644
index 0000000..e8897b7
--- /dev/null
+++ b/src/components/atoms/icons/plus-minus.tsx
@@ -0,0 +1,31 @@
+import { FC } from 'react';
+import styles from './plus-minus.module.scss';
+
+export type PlusMinusProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+ /**
+ * Which state should be displayed.
+ */
+ state: 'plus' | 'minus';
+};
+
+/**
+ * PlusMinus component
+ *
+ * Render a plus or a minus icon.
+ */
+const PlusMinus: FC<PlusMinusProps> = ({ className, state }) => {
+ const stateClass = `icon--${state}`;
+
+ return (
+ <div
+ className={`${styles.icon} ${styles[stateClass]} ${className}`}
+ aria-hidden={true}
+ ></div>
+ );
+};
+
+export default PlusMinus;
diff --git a/src/components/Icons/Blog/Blog.module.scss b/src/components/atoms/icons/posts-stack.module.scss
index 5376c61..a22d265 100644
--- a/src/components/Icons/Blog/Blog.module.scss
+++ b/src/components/atoms/icons/posts-stack.module.scss
@@ -2,7 +2,6 @@
.icon {
display: block;
- margin: auto;
width: var(--icon-size, #{fun.convert-px(40)});
}
diff --git a/src/components/atoms/icons/posts-stack.stories.tsx b/src/components/atoms/icons/posts-stack.stories.tsx
new file mode 100644
index 0000000..1990b98
--- /dev/null
+++ b/src/components/atoms/icons/posts-stack.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import PostsStackIcon from './posts-stack';
+
+/**
+ * Posts Stack icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: PostsStackIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof PostsStackIcon>;
+
+const Template: ComponentStory<typeof PostsStackIcon> = (args) => (
+ <PostsStackIcon {...args} />
+);
+
+/**
+ * Icons Stories - Posts Stack
+ */
+export const PostsStack = Template.bind({});
diff --git a/src/components/atoms/icons/posts-stack.test.tsx b/src/components/atoms/icons/posts-stack.test.tsx
new file mode 100644
index 0000000..8f44fa9
--- /dev/null
+++ b/src/components/atoms/icons/posts-stack.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import PostsStack from './posts-stack';
+
+describe('PostsStack', () => {
+ it('renders a posts stack icon', () => {
+ const { container } = render(<PostsStack />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Blog/Blog.tsx b/src/components/atoms/icons/posts-stack.tsx
index bd32111..ab21323 100644
--- a/src/components/Icons/Blog/Blog.tsx
+++ b/src/components/atoms/icons/posts-stack.tsx
@@ -1,11 +1,24 @@
-import styles from './Blog.module.scss';
+import { FC } from 'react';
+import styles from './posts-stack.module.scss';
-const BlogIcon = () => {
+export type PostsStackProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+};
+
+/**
+ * Posts stack component.
+ *
+ * Render a posts stack svg icon.
+ */
+const PostsStack: FC<PostsStackProps> = ({ className = '' }) => {
return (
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
- className={styles.icon}
+ className={`${styles.icon} ${className}`}
>
<path
className={styles.background}
@@ -59,4 +72,4 @@ const BlogIcon = () => {
);
};
-export default BlogIcon;
+export default PostsStack;
diff --git a/src/components/Icons/Sun/Sun.module.scss b/src/components/atoms/icons/sun.module.scss
index 5682aa3..5682aa3 100644
--- a/src/components/Icons/Sun/Sun.module.scss
+++ b/src/components/atoms/icons/sun.module.scss
diff --git a/src/components/atoms/icons/sun.stories.tsx b/src/components/atoms/icons/sun.stories.tsx
new file mode 100644
index 0000000..60af648
--- /dev/null
+++ b/src/components/atoms/icons/sun.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SunIcon from './sun';
+
+/**
+ * Sun icon - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Icons',
+ component: SunIcon,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The SVG title.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof SunIcon>;
+
+const Template: ComponentStory<typeof SunIcon> = (args) => (
+ <SunIcon {...args} />
+);
+
+/**
+ * Icons Stories - Sun
+ */
+export const Sun = Template.bind({});
diff --git a/src/components/atoms/icons/sun.test.tsx b/src/components/atoms/icons/sun.test.tsx
new file mode 100644
index 0000000..21661a9
--- /dev/null
+++ b/src/components/atoms/icons/sun.test.tsx
@@ -0,0 +1,9 @@
+import { render } from '@test-utils';
+import Sun from './sun';
+
+describe('Sun', () => {
+ it('renders a sun icon', () => {
+ const { container } = render(<Sun />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/Icons/Sun/Sun.tsx b/src/components/atoms/icons/sun.tsx
index 12f47d3..ca31747 100644
--- a/src/components/Icons/Sun/Sun.tsx
+++ b/src/components/atoms/icons/sun.tsx
@@ -1,25 +1,33 @@
-import { useIntl } from 'react-intl';
-import styles from './Sun.module.scss';
+import { FC } from 'react';
+import styles from './sun.module.scss';
-const SunIcon = () => {
- const intl = useIntl();
+export type SunProps = {
+ /**
+ * Set additional classnames to the icon.
+ */
+ className?: string;
+ /**
+ * The SVG title.
+ */
+ title?: string;
+};
+/**
+ * Sun component.
+ *
+ * Render a svg sun icon.
+ */
+const Sun: FC<SunProps> = ({ className = '', title }) => {
return (
<svg
- className={styles.sun}
+ className={`${styles.sun} ${className}`}
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
- <title>
- {intl.formatMessage({
- defaultMessage: 'Light theme',
- description: 'Icons: Sun icon (light theme)',
- id: 'KeRtm/',
- })}
- </title>
+ {title !== 'undefined' && <title>{title}</title>}
<path d="M 69.398043,50.000437 A 19.399259,19.399204 0 0 1 49.998784,69.399641 19.399259,19.399204 0 0 1 30.599525,50.000437 19.399259,19.399204 0 0 1 49.998784,30.601234 19.399259,19.399204 0 0 1 69.398043,50.000437 Z m 27.699233,1.125154 c 2.657696,0.0679 1.156196,12.061455 -1.435545,11.463959 L 80.113224,59.000697 c -2.589801,-0.597494 -1.625657,-8.345536 1.032041,-8.278609 z m -18.06653,37.251321 c 1.644087,2.091234 -9.030355,8.610337 -10.126414,6.188346 L 62.331863,80.024585 c -1.096058,-2.423931 5.197062,-6.285342 6.839209,-4.194107 z M 38.611418,97.594444 C 38.02653,100.18909 26.24148,95.916413 27.436475,93.54001 l 7.168026,-14.256474 c 1.194024,-2.376403 8.102101,0.151313 7.517214,2.744986 z M 6.1661563,71.834242 C 3.7916868,73.028262 -0.25499873,61.16274 2.3386824,60.577853 L 17.905618,57.067567 c 2.593681,-0.584886 4.894434,6.403678 2.518995,7.598668 z M 6.146757,30.055146 c -2.3764094,-1.194991 4.46571,-11.714209 6.479353,-9.97798 l 12.090589,10.414462 c 2.014613,1.736229 -1.937017,7.926514 -4.314396,6.731524 z M 38.56777,4.2639045 C 37.982883,1.6682911 50.480855,0.41801247 50.415868,3.0766733 L 50.020123,19.028638 c -0.06596,2.657691 -7.357169,3.394862 -7.943027,0.800218 z m 40.403808,9.1622435 c 1.635357,-2.098023 10.437771,6.872168 8.339742,8.506552 l -12.58818,9.805327 c -2.099,1.634383 -7.192276,-3.626682 -5.557888,-5.724706 z M 97.096306,50.69105 c 2.657696,-0.06596 1.164926,12.462047 -1.425846,11.863582 L 80.122924,58.96578 c -2.590771,-0.597496 -1.636327,-7.814 1.021371,-7.879957 z" />
</svg>
);
};
-export default SunIcon;
+export default Sun;
diff --git a/src/components/Branding/Logo/Logo.module.scss b/src/components/atoms/images/logo.module.scss
index 3d62bf9..f802a4b 100644
--- a/src/components/Branding/Logo/Logo.module.scss
+++ b/src/components/atoms/images/logo.module.scss
@@ -1,5 +1,10 @@
+@use "@styles/abstracts/functions" as fun;
+
.wrapper {
- position: relative;
+ width: var(--logo-size, fun.convert-px(100));
+ height: var(--logo-size, fun.convert-px(100));
+ max-width: 100%;
+ max-height: 100%;
}
.bg-left {
diff --git a/src/components/atoms/images/logo.stories.tsx b/src/components/atoms/images/logo.stories.tsx
new file mode 100644
index 0000000..458ec08
--- /dev/null
+++ b/src/components/atoms/images/logo.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import LogoComponent from './logo';
+
+/**
+ * Logo - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Illustrations/Images',
+ component: LogoComponent,
+ argTypes: {
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The SVG title.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof LogoComponent>;
+
+const Template: ComponentStory<typeof LogoComponent> = (args) => (
+ <LogoComponent {...args} />
+);
+
+/**
+ * Images Stories - Logo
+ */
+export const Logo = Template.bind({});
diff --git a/src/components/atoms/images/logo.test.tsx b/src/components/atoms/images/logo.test.tsx
new file mode 100644
index 0000000..3e0d238
--- /dev/null
+++ b/src/components/atoms/images/logo.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import Logo from './logo';
+
+describe('Logo', () => {
+ it('renders a logo with a title', () => {
+ render(<Logo title="My title" />);
+ expect(screen.getByTitle('My title')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/Branding/Logo/Logo.tsx b/src/components/atoms/images/logo.tsx
index 0623042..3978882 100644
--- a/src/components/Branding/Logo/Logo.tsx
+++ b/src/components/atoms/images/logo.tsx
@@ -1,12 +1,26 @@
-import styles from './Logo.module.scss';
+import { FC } from 'react';
+import styles from './logo.module.scss';
-const Logo = () => {
+export type LogoProps = {
+ /**
+ * SVG Image title.
+ */
+ title?: string;
+};
+
+/**
+ * Logo component.
+ *
+ * Render a SVG logo.
+ */
+const Logo: FC<LogoProps> = ({ title }) => {
return (
<svg
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
className={styles.wrapper}
>
+ {title && <title>{title}</title>}
<path className={styles['bg-left']} d="M 0,0 H 506 L 0,506 Z" />
<path className={styles['bg-right']} d="M 512,512 H 6 L 512,6 Z" />
<path
diff --git a/src/components/atoms/layout/column.stories.tsx b/src/components/atoms/layout/column.stories.tsx
new file mode 100644
index 0000000..a03c462
--- /dev/null
+++ b/src/components/atoms/layout/column.stories.tsx
@@ -0,0 +1,29 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ColumnComponent from './column';
+
+export default {
+ title: 'Atoms/Layout/Column',
+ component: ColumnComponent,
+ argTypes: {
+ children: {
+ description: 'The column body.',
+ type: {
+ name: 'array',
+ required: true,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof ColumnComponent>;
+
+const Template: ComponentStory<typeof ColumnComponent> = (args) => (
+ <ColumnComponent {...args} />
+);
+
+const body =
+ 'Non praesentium voluptas quisquam ex est. Distinctio accusamus facilis libero in aut. Et veritatis quo impedit fugit amet sit accusantium. Ut est rerum asperiores sint libero eveniet. Molestias placeat recusandae suscipit eligendi sunt hic.';
+
+export const Column = Template.bind({});
+Column.args = {
+ children: body,
+};
diff --git a/src/components/atoms/layout/column.test.tsx b/src/components/atoms/layout/column.test.tsx
new file mode 100644
index 0000000..c5c6554
--- /dev/null
+++ b/src/components/atoms/layout/column.test.tsx
@@ -0,0 +1,12 @@
+import { render, screen } from '@test-utils';
+import Column from './column';
+
+const body =
+ 'Non praesentium voluptas quisquam ex est. Distinctio accusamus facilis libero in aut. Et veritatis quo impedit fugit amet sit accusantium. Ut est rerum asperiores sint libero eveniet. Molestias placeat recusandae suscipit eligendi sunt hic.';
+
+describe('Column', () => {
+ it('renders the column body', () => {
+ render(<Column>{body}</Column>);
+ expect(screen.getByText(body)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/atoms/layout/column.tsx b/src/components/atoms/layout/column.tsx
new file mode 100644
index 0000000..ec6440d
--- /dev/null
+++ b/src/components/atoms/layout/column.tsx
@@ -0,0 +1,16 @@
+import { FC, ReactNode } from 'react';
+
+export type ColumnProps = {
+ children: ReactNode | ReactNode[];
+};
+
+/**
+ * Column component.
+ *
+ * Render the body as a column.
+ */
+const Column: FC<ColumnProps> = ({ children }) => {
+ return <div>{children}</div>;
+};
+
+export default Column;
diff --git a/src/components/Copyright/Copyright.module.scss b/src/components/atoms/layout/copyright.module.scss
index 35445b2..a0e5347 100644
--- a/src/components/Copyright/Copyright.module.scss
+++ b/src/components/atoms/layout/copyright.module.scss
@@ -16,18 +16,17 @@
@include mix.media("screen") {
@include mix.dimensions("sm") {
- place-content: start;
text-align: left;
}
}
}
-.name {
+.owner {
flex: 1 0 100%;
@include mix.media("screen") {
@include mix.dimensions("sm") {
- flex: auto;
+ flex: initial;
}
}
}
diff --git a/src/components/atoms/layout/copyright.stories.tsx b/src/components/atoms/layout/copyright.stories.tsx
new file mode 100644
index 0000000..612b114
--- /dev/null
+++ b/src/components/atoms/layout/copyright.stories.tsx
@@ -0,0 +1,58 @@
+import CCBySA from '@components/atoms/icons/cc-by-sa';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CopyrightComponent from './copyright';
+
+/**
+ * Copyright - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Layout',
+ component: CopyrightComponent,
+ argTypes: {
+ dates: {
+ description: 'The copyright dates.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ icon: {
+ control: {
+ type: null,
+ },
+ description: 'The copyright icon.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ owner: {
+ control: {
+ type: 'text',
+ },
+ description: 'The copyright owner',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof CopyrightComponent>;
+
+const Template: ComponentStory<typeof CopyrightComponent> = (args) => (
+ <CopyrightComponent {...args} />
+);
+
+/**
+ * Layout Stories - Copyright
+ */
+export const Copyright = Template.bind({});
+Copyright.args = {
+ dates: {
+ start: '2012',
+ end: '2022',
+ },
+ icon: <CCBySA />,
+ owner: 'Your name',
+};
diff --git a/src/components/atoms/layout/copyright.test.tsx b/src/components/atoms/layout/copyright.test.tsx
new file mode 100644
index 0000000..6bfe612
--- /dev/null
+++ b/src/components/atoms/layout/copyright.test.tsx
@@ -0,0 +1,32 @@
+import CCBySA from '@components/atoms/icons/cc-by-sa';
+import { render, screen } from '@test-utils';
+import Copyright from './copyright';
+
+const dates = {
+ start: '2012',
+ end: '2022',
+};
+const icon = <CCBySA />;
+const owner = 'Your name';
+
+describe('Copyright', () => {
+ it('renders the copyright owner', () => {
+ render(<Copyright dates={dates} icon={icon} owner={owner} />);
+ expect(screen.getByText(owner)).toBeInTheDocument();
+ });
+
+ it('renders the copyright start date', () => {
+ render(<Copyright dates={dates} icon={icon} owner={owner} />);
+ expect(screen.getByText(dates.start)).toBeInTheDocument();
+ });
+
+ it('renders the copyright end date', () => {
+ render(<Copyright dates={dates} icon={icon} owner={owner} />);
+ expect(screen.getByText(dates.end)).toBeInTheDocument();
+ });
+
+ it('renders the copyright icon', () => {
+ render(<Copyright dates={dates} icon={icon} owner={owner} />);
+ expect(screen.getByTitle('CC BY SA')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/atoms/layout/copyright.tsx b/src/components/atoms/layout/copyright.tsx
new file mode 100644
index 0000000..f70695d
--- /dev/null
+++ b/src/components/atoms/layout/copyright.tsx
@@ -0,0 +1,59 @@
+import { FC, ReactNode } from 'react';
+import styles from './copyright.module.scss';
+
+export type CopyrightDates = {
+ /**
+ * The copyright start year.
+ */
+ start: string;
+ /**
+ * The copyright end year.
+ */
+ end?: string;
+};
+
+export type CopyrightProps = {
+ /**
+ * The copyright owner.
+ */
+ owner: string;
+ /**
+ * The copyright dates.
+ */
+ dates: CopyrightDates;
+ /**
+ * The copyright icon.
+ */
+ icon: ReactNode;
+};
+
+/**
+ * Copyright component
+ *
+ * Renders a copyright information (owner, dates, license icon).
+ */
+const Copyright: FC<CopyrightProps> = ({ owner, dates, icon }) => {
+ const getFormattedDate = (date: string) => {
+ const datetime = new Date(date).toISOString();
+
+ return <time dateTime={datetime}>{date}</time>;
+ };
+
+ return (
+ <div className={styles.wrapper}>
+ <span className={styles.owner}>{owner}</span>
+ {icon}
+ {getFormattedDate(dates.start)}
+ {dates.end ? (
+ <>
+ <span>-</span>
+ {getFormattedDate(dates.end)}
+ </>
+ ) : (
+ ''
+ )}
+ </div>
+ );
+};
+
+export default Copyright;
diff --git a/src/components/atoms/layout/main.stories.tsx b/src/components/atoms/layout/main.stories.tsx
new file mode 100644
index 0000000..5bde475
--- /dev/null
+++ b/src/components/atoms/layout/main.stories.tsx
@@ -0,0 +1,58 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import MainComponent from './main';
+
+/**
+ * Main - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Layout',
+ component: MainComponent,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The content.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the main element.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'The main element id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof MainComponent>;
+
+const Template: ComponentStory<typeof MainComponent> = (args) => (
+ <MainComponent {...args} />
+);
+
+/**
+ * Layout Stories - Main
+ */
+export const Main = Template.bind({});
+Main.args = {
+ children: 'The main content.',
+ id: '#main',
+};
diff --git a/src/components/atoms/layout/main.test.tsx b/src/components/atoms/layout/main.test.tsx
new file mode 100644
index 0000000..f91846f
--- /dev/null
+++ b/src/components/atoms/layout/main.test.tsx
@@ -0,0 +1,12 @@
+import { render, screen } from '@test-utils';
+import Main from './main';
+
+const id = 'main';
+const children = 'The main content.';
+
+describe('Main', () => {
+ it('renders the content of main element', () => {
+ render(<Main id={id}>{children}</Main>);
+ expect(screen.getByRole('main')).toHaveTextContent(children);
+ });
+});
diff --git a/src/components/atoms/layout/main.tsx b/src/components/atoms/layout/main.tsx
new file mode 100644
index 0000000..d92a5c7
--- /dev/null
+++ b/src/components/atoms/layout/main.tsx
@@ -0,0 +1,27 @@
+import { FC, ReactNode } from 'react';
+
+export type MainProps = {
+ /**
+ * The main body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames to the main element.
+ */
+ className?: string;
+ /**
+ * The main wrapper id.
+ */
+ id: string;
+};
+
+/**
+ * Main component
+ *
+ * Render a main element.
+ */
+const Main: FC<MainProps> = ({ children, ...props }) => {
+ return <main {...props}>{children}</main>;
+};
+
+export default Main;
diff --git a/src/components/atoms/layout/no-script.module.scss b/src/components/atoms/layout/no-script.module.scss
new file mode 100644
index 0000000..d8712af
--- /dev/null
+++ b/src/components/atoms/layout/no-script.module.scss
@@ -0,0 +1,19 @@
+@use "@styles/abstracts/functions" as fun;
+
+.noscript {
+ color: var(--color-primary-darker);
+
+ &--top {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 10;
+ background: var(--color-bg);
+ border-bottom: fun.convert-px(3) solid var(--color-border);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ text-align: center;
+ }
+}
diff --git a/src/components/atoms/layout/no-script.stories.tsx b/src/components/atoms/layout/no-script.stories.tsx
new file mode 100644
index 0000000..22d2fea
--- /dev/null
+++ b/src/components/atoms/layout/no-script.stories.tsx
@@ -0,0 +1,62 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import NoScript from './no-script';
+
+/**
+ * NoScript - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Layout/NoScript',
+ component: NoScript,
+ args: {
+ position: 'initial',
+ },
+ argTypes: {
+ message: {
+ control: {
+ type: 'text',
+ },
+ description: 'A message to display when Javascript is disabled.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ position: {
+ control: {
+ type: 'select',
+ },
+ description: 'The message position.',
+ options: ['initial', 'top'],
+ table: {
+ category: 'Options',
+ defaultValue: 'initial',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof NoScript>;
+
+const Template: ComponentStory<typeof NoScript> = (args) => (
+ <NoScript {...args} />
+);
+
+/**
+ * NoScript Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ message: 'A noscript only message.',
+ position: 'initial',
+};
+
+/**
+ * NoScript Stories - Top
+ */
+export const Top = Template.bind({});
+Top.args = {
+ message: 'A noscript only message.',
+ position: 'top',
+};
diff --git a/src/components/atoms/layout/no-script.test.tsx b/src/components/atoms/layout/no-script.test.tsx
new file mode 100644
index 0000000..9ed9c4c
--- /dev/null
+++ b/src/components/atoms/layout/no-script.test.tsx
@@ -0,0 +1,11 @@
+import { render, screen } from '@test-utils';
+import NoScript from './no-script';
+
+const message = 'A noscript message.';
+
+describe('NoScript', () => {
+ it('renders a message', () => {
+ render(<NoScript message={message} />);
+ expect(screen.getByText(message)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/atoms/layout/no-script.tsx b/src/components/atoms/layout/no-script.tsx
new file mode 100644
index 0000000..a503e0c
--- /dev/null
+++ b/src/components/atoms/layout/no-script.tsx
@@ -0,0 +1,21 @@
+import { FC } from 'react';
+import styles from './no-script.module.scss';
+
+export type NoScriptProps = {
+ /**
+ * The noscript message.
+ */
+ message: string;
+ /**
+ * The message position. Default: initial.
+ */
+ position?: 'initial' | 'top';
+};
+
+const NoScript: FC<NoScriptProps> = ({ message, position = 'initial' }) => {
+ const positionClass = styles[`noscript--${position}`];
+
+ return <div className={`${styles.noscript} ${positionClass}`}>{message}</div>;
+};
+
+export default NoScript;
diff --git a/src/components/Notice/Notice.module.scss b/src/components/atoms/layout/notice.module.scss
index aa7175c..7fd972c 100644
--- a/src/components/Notice/Notice.module.scss
+++ b/src/components/atoms/layout/notice.module.scss
@@ -1,10 +1,9 @@
@use "@styles/abstracts/functions" as fun;
-.message {
+.wrapper {
+ padding: var(--spacing-2xs) var(--spacing-xs);
border: fun.convert-px(2) solid;
font-weight: bold;
- margin: var(--spacing-sm) auto;
- padding: var(--spacing-2xs) var(--spacing-xs);
&--error {
border-color: var(--color-token-red);
diff --git a/src/components/atoms/layout/notice.stories.tsx b/src/components/atoms/layout/notice.stories.tsx
new file mode 100644
index 0000000..dedf834
--- /dev/null
+++ b/src/components/atoms/layout/notice.stories.tsx
@@ -0,0 +1,86 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import NoticeComponent from './notice';
+
+/**
+ * Notice - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Layout/Notice',
+ component: NoticeComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the notice wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The notice kind.',
+ options: ['error', 'info', 'success', 'warning'],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ message: {
+ control: {
+ type: 'text',
+ },
+ description: 'The notice body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof NoticeComponent>;
+
+const Template: ComponentStory<typeof NoticeComponent> = (args) => (
+ <NoticeComponent {...args} />
+);
+
+/**
+ * Notice stories - Error
+ */
+export const Error = Template.bind({});
+Error.args = {
+ kind: 'error',
+ message: 'Nisi provident sapiente.',
+};
+
+/**
+ * Notice stories - Info
+ */
+export const Info = Template.bind({});
+Info.args = {
+ kind: 'info',
+ message: 'Nisi provident sapiente.',
+};
+
+/**
+ * Notice stories - Success
+ */
+export const Success = Template.bind({});
+Success.args = {
+ kind: 'success',
+ message: 'Nisi provident sapiente.',
+};
+
+/**
+ * Notice stories - Warning
+ */
+export const Warning = Template.bind({});
+Warning.args = {
+ kind: 'warning',
+ message: 'Nisi provident sapiente.',
+};
diff --git a/src/components/atoms/layout/notice.test.tsx b/src/components/atoms/layout/notice.test.tsx
new file mode 100644
index 0000000..4501f8f
--- /dev/null
+++ b/src/components/atoms/layout/notice.test.tsx
@@ -0,0 +1,11 @@
+import { render, screen } from '@test-utils';
+import Notice from './notice';
+
+const message = 'Tenetur consequuntur tempore.';
+
+describe('Notice', () => {
+ it('renders a message', () => {
+ render(<Notice kind="info" message={message} />);
+ expect(screen.getByText(message)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/atoms/layout/notice.tsx b/src/components/atoms/layout/notice.tsx
new file mode 100644
index 0000000..a0d1d3e
--- /dev/null
+++ b/src/components/atoms/layout/notice.tsx
@@ -0,0 +1,38 @@
+import { FC } from 'react';
+import styles from './notice.module.scss';
+
+export type NoticeKind = 'error' | 'info' | 'success' | 'warning';
+
+export type NoticeProps = {
+ /**
+ * Set additional classnames to the notice wrapper.
+ */
+ className?: string;
+ /**
+ * The notice kind.
+ */
+ kind: NoticeKind;
+ /**
+ * The notice body.
+ */
+ message: string;
+};
+
+/**
+ * Notice component
+ *
+ * Render a colored message depending on notice kind.
+ */
+const Notice: FC<NoticeProps> = ({ className = '', kind, message }) => {
+ const kindClass = `wrapper--${kind}`;
+
+ return message ? (
+ <div className={`${styles.wrapper} ${styles[kindClass]} ${className}`}>
+ {message}
+ </div>
+ ) : (
+ <></>
+ );
+};
+
+export default Notice;
diff --git a/src/components/atoms/layout/section.module.scss b/src/components/atoms/layout/section.module.scss
new file mode 100644
index 0000000..012493a
--- /dev/null
+++ b/src/components/atoms/layout/section.module.scss
@@ -0,0 +1,25 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/placeholders";
+
+.wrapper {
+ @extend %grid;
+
+ padding: var(--spacing-md) 0;
+
+ &--borders {
+ border-bottom: fun.convert-px(1) solid var(--color-border);
+ }
+
+ &--dark {
+ background: var(--color-bg-secondary);
+ }
+
+ &--light {
+ background: var(--color-bg);
+ }
+}
+
+.body,
+.title {
+ grid-column: 2;
+}
diff --git a/src/components/atoms/layout/section.stories.tsx b/src/components/atoms/layout/section.stories.tsx
new file mode 100644
index 0000000..530b2a0
--- /dev/null
+++ b/src/components/atoms/layout/section.stories.tsx
@@ -0,0 +1,102 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Section from './section';
+
+/**
+ * Section - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Layout/Section',
+ component: Section,
+ args: {
+ variant: 'dark',
+ withBorder: true,
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the section element.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ content: {
+ control: {
+ type: 'text',
+ },
+ description: 'The section content.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The section title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ variant: {
+ control: {
+ type: 'select',
+ },
+ description: 'The section variant.',
+ options: ['light', 'dark'],
+ table: {
+ category: 'Styles',
+ defaultValue: { summary: 'dark' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ withBorder: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Add a border at the bottom of the section.',
+ table: {
+ category: 'Styles',
+ defaultValue: { summary: true },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Section>;
+
+const Template: ComponentStory<typeof Section> = (args) => (
+ <Section {...args} />
+);
+
+/**
+ * Section Stories - Light
+ */
+export const Light = Template.bind({});
+Light.args = {
+ title: 'A title',
+ content: 'The content.',
+ variant: 'light',
+};
+
+/**
+ * Section Stories - Dark
+ */
+export const Dark = Template.bind({});
+Dark.args = {
+ title: 'A title',
+ content: 'The content.',
+ variant: 'dark',
+};
diff --git a/src/components/atoms/layout/section.test.tsx b/src/components/atoms/layout/section.test.tsx
new file mode 100644
index 0000000..ca5f03a
--- /dev/null
+++ b/src/components/atoms/layout/section.test.tsx
@@ -0,0 +1,17 @@
+import { render, screen } from '@test-utils';
+import Section from './section';
+
+const title = 'Section title';
+const content = 'Section content.';
+
+describe('Section', () => {
+ it('renders a title (h2)', () => {
+ render(<Section title={title} content={content} />);
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(title);
+ });
+
+ it('renders a content', () => {
+ render(<Section title={title} content={content} />);
+ expect(screen.getByText(content)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/atoms/layout/section.tsx b/src/components/atoms/layout/section.tsx
new file mode 100644
index 0000000..cb727ff
--- /dev/null
+++ b/src/components/atoms/layout/section.tsx
@@ -0,0 +1,57 @@
+import { FC, ReactNode } from 'react';
+import Heading from '../headings/heading';
+import styles from './section.module.scss';
+
+export type SectionVariant = 'dark' | 'light';
+
+export type SectionProps = {
+ /**
+ * Set additional classnames to the section element.
+ */
+ className?: string;
+ /**
+ * The section content.
+ */
+ content: ReactNode;
+ /**
+ * The section title.
+ */
+ title: string;
+ /**
+ * The section variant.
+ */
+ variant?: SectionVariant;
+ /**
+ * Add a border at the bottom of the section. Default: true.
+ */
+ withBorder?: boolean;
+};
+
+/**
+ * Section component
+ *
+ * Render a section element.
+ */
+const Section: FC<SectionProps> = ({
+ className = '',
+ content,
+ title,
+ variant = 'dark',
+ withBorder = true,
+}) => {
+ const borderClass = withBorder ? styles[`wrapper--borders`] : '';
+ const variantClass = styles[`wrapper--${variant}`];
+
+ return (
+ <section
+ className={`${styles.wrapper} ${borderClass} ${variantClass} ${className}`}
+ >
+ <Heading level={2} className={styles.title}>
+ {title}
+ </Heading>
+ <div className={styles.body}>{content}</div>
+ </section>
+ );
+};
+
+export default Section;
diff --git a/src/components/atoms/layout/sidebar.module.scss b/src/components/atoms/layout/sidebar.module.scss
new file mode 100644
index 0000000..5d36f18
--- /dev/null
+++ b/src/components/atoms/layout/sidebar.module.scss
@@ -0,0 +1,12 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ > *:not(:first-child) {
+ margin-top: fun.convert-px(-2);
+ }
+}
+
+.body {
+ position: sticky;
+ top: var(--spacing-xs);
+}
diff --git a/src/components/atoms/layout/sidebar.stories.tsx b/src/components/atoms/layout/sidebar.stories.tsx
new file mode 100644
index 0000000..6876f95
--- /dev/null
+++ b/src/components/atoms/layout/sidebar.stories.tsx
@@ -0,0 +1,60 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SidebarComponent from './sidebar';
+
+/**
+ * Sidebar - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Layout',
+ component: SidebarComponent,
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible name for the sidebar.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The sidebar content.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the aside element.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof SidebarComponent>;
+
+const Template: ComponentStory<typeof SidebarComponent> = (args) => (
+ <SidebarComponent {...args} />
+);
+
+/**
+ * Layout Stories - Sidebar
+ */
+export const Sidebar = Template.bind({});
+Sidebar.args = {
+ children: 'Some widgets.',
+};
diff --git a/src/components/atoms/layout/sidebar.test.tsx b/src/components/atoms/layout/sidebar.test.tsx
new file mode 100644
index 0000000..4c9459d
--- /dev/null
+++ b/src/components/atoms/layout/sidebar.test.tsx
@@ -0,0 +1,11 @@
+import { render, screen } from '@test-utils';
+import Sidebar from './sidebar';
+
+const children = 'A widget';
+
+describe('Sidebar', () => {
+ it('renders an aside element', () => {
+ render(<Sidebar>{children}</Sidebar>);
+ expect(screen.getByRole('complementary')).toHaveTextContent(children);
+ });
+});
diff --git a/src/components/atoms/layout/sidebar.tsx b/src/components/atoms/layout/sidebar.tsx
new file mode 100644
index 0000000..d86af37
--- /dev/null
+++ b/src/components/atoms/layout/sidebar.tsx
@@ -0,0 +1,32 @@
+import { FC, ReactNode } from 'react';
+import styles from './sidebar.module.scss';
+
+export type SidebarProps = {
+ /**
+ * An accessible name for the sidebar.
+ */
+ 'aria-label'?: string;
+ /**
+ * The sidebar body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames to the aside element.
+ */
+ className?: string;
+};
+
+/**
+ * Sidebar component
+ *
+ * Render an aside element.
+ */
+const Sidebar: FC<SidebarProps> = ({ children, className = '', ...props }) => {
+ return (
+ <aside className={`${styles.wrapper} ${className}`} {...props}>
+ <div className={styles.body}>{children}</div>
+ </aside>
+ );
+};
+
+export default Sidebar;
diff --git a/src/components/atoms/links/link.module.scss b/src/components/atoms/links/link.module.scss
new file mode 100644
index 0000000..bb5775f
--- /dev/null
+++ b/src/components/atoms/links/link.module.scss
@@ -0,0 +1,220 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/variables" as var;
+
+/* stylelint-disable no-descending-specificity */
+.link {
+ &[hreflang] {
+ &::after {
+ display: inline-block;
+
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]";
+ font-size: var(--font-size-sm);
+ }
+ }
+
+ &--download {
+ &::after {
+ display: inline-block;
+
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_white}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_white}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+ }
+ }
+
+ &--external {
+ &::after {
+ display: inline-block;
+
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+
+ &--external#{&}--download {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_white}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_white}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+}
+
+:global([data-theme="dark"]) {
+ :local {
+ .link {
+ &--download {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+ }
+ }
+
+ &--external {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+
+ &--external.link--download {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+ }
+ }
+}
+/* stylelint-enable no-descending-specificity */
diff --git a/src/components/atoms/links/link.stories.tsx b/src/components/atoms/links/link.stories.tsx
new file mode 100644
index 0000000..4baabe5
--- /dev/null
+++ b/src/components/atoms/links/link.stories.tsx
@@ -0,0 +1,180 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Link from './link';
+
+/**
+ * Link - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Typography/Links',
+ component: Link,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ download: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the link purpose is to download a file.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ external: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the link is external of the current website.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ href: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link target.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ lang: {
+ control: {
+ type: 'text',
+ },
+ table: {
+ category: 'Options',
+ },
+ description: 'The target language as code language.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Link>;
+
+const Template: ComponentStory<typeof Link> = (args) => <Link {...args} />;
+
+/**
+ * Links Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ children: 'A link',
+ href: '#',
+ download: false,
+ external: false,
+};
+
+/**
+ * Links Stories - Download
+ */
+export const Download = Template.bind({});
+Download.args = {
+ children: 'A link to a file',
+ href: '#',
+ download: true,
+ external: false,
+};
+
+/**
+ * Links Stories - DownloadWithLang
+ */
+export const DownloadWithLang = Template.bind({});
+DownloadWithLang.args = {
+ children: 'A link to a file',
+ href: '#',
+ download: true,
+ external: false,
+ lang: 'en',
+};
+
+/**
+ * Links Stories - External
+ */
+export const External = Template.bind({});
+External.args = {
+ children: 'A link',
+ href: '#',
+ download: false,
+ external: true,
+};
+
+/**
+ * Links Stories - External download
+ */
+export const ExternalDownload = Template.bind({});
+ExternalDownload.args = {
+ children: 'A link',
+ href: '#',
+ download: true,
+ external: true,
+};
+
+/**
+ * Links Stories - External With Lang
+ */
+export const ExternalWithLang = Template.bind({});
+ExternalWithLang.args = {
+ children: 'A link',
+ href: '#',
+ download: false,
+ external: true,
+ lang: 'en',
+};
+
+/**
+ * Links Stories - External download with lang
+ */
+export const ExternalDownloadWithLang = Template.bind({});
+ExternalDownloadWithLang.args = {
+ children: 'A link',
+ href: '#',
+ download: true,
+ external: true,
+ lang: 'en',
+};
+
+/**
+ * Links Stories - With Lang
+ */
+export const WithLang = Template.bind({});
+WithLang.args = {
+ children: 'A link',
+ href: '#',
+ download: false,
+ external: false,
+ lang: 'en',
+};
diff --git a/src/components/atoms/links/link.test.tsx b/src/components/atoms/links/link.test.tsx
new file mode 100644
index 0000000..54e2414
--- /dev/null
+++ b/src/components/atoms/links/link.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import Link from './link';
+
+describe('Link', () => {
+ it('render a link', () => {
+ render(<Link href="#">A link</Link>);
+ expect(screen.getByRole('link')).toHaveTextContent('A link');
+ });
+});
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
new file mode 100644
index 0000000..c8ba273
--- /dev/null
+++ b/src/components/atoms/links/link.tsx
@@ -0,0 +1,67 @@
+import NextLink from 'next/link';
+import { FC, ReactNode } from 'react';
+import styles from './link.module.scss';
+
+export type LinkProps = {
+ /**
+ * The link body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames to the link.
+ */
+ className?: string;
+ /**
+ * True if it is a download link. Default: false.
+ */
+ download?: boolean;
+ /**
+ * True if it is an external link. Default: false.
+ */
+ external?: boolean;
+ /**
+ * The link target.
+ */
+ href: string;
+ /**
+ * The link target code language.
+ */
+ lang?: string;
+};
+
+/**
+ * Link Component
+ *
+ * Render a link.
+ */
+const Link: FC<LinkProps> = ({
+ children,
+ className = '',
+ download = false,
+ external = false,
+ href,
+ lang,
+}) => {
+ const downloadClass = download ? styles['link--download'] : '';
+
+ return external ? (
+ <a
+ href={href}
+ hrefLang={lang}
+ className={`${styles.link} ${styles['link--external']} ${downloadClass} ${className}`}
+ >
+ {children}
+ </a>
+ ) : (
+ <NextLink href={href}>
+ <a
+ hrefLang={lang}
+ className={`${styles.link} ${downloadClass} ${className}`}
+ >
+ {children}
+ </a>
+ </NextLink>
+ );
+};
+
+export default Link;
diff --git a/src/components/atoms/links/nav-link.module.scss b/src/components/atoms/links/nav-link.module.scss
new file mode 100644
index 0000000..241c9c3
--- /dev/null
+++ b/src/components/atoms/links/nav-link.module.scss
@@ -0,0 +1,46 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.link {
+ --draw-border-thickness: #{fun.convert-px(4)};
+ --draw-border-color1: var(--color-primary-light);
+ --draw-border-color2: var(--color-primary-lighter);
+ --icon-size: #{fun.convert-px(30)};
+
+ display: inline-flex;
+ flex-flow: column nowrap;
+ place-items: center;
+ place-content: center;
+ row-gap: var(--spacing-2xs);
+ min-width: var(--link-min-width, fun.convert-px(85));
+ padding: var(--spacing-xs);
+ background: inherit;
+ font-size: var(--font-size-sm);
+ font-variant: small-caps;
+ font-weight: 600;
+ line-height: 1;
+ text-decoration: none;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ border-radius: 8%;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ @extend %draw-borders;
+ }
+
+ &:focus {
+ color: var(--color-primary-light);
+ }
+
+ &:active {
+ --draw-border-color1: var(--color-primary-dark);
+ --draw-border-color2: var(--color-primary-light);
+
+ @extend %draw-borders;
+ }
+}
diff --git a/src/components/atoms/links/nav-link.stories.tsx b/src/components/atoms/links/nav-link.stories.tsx
new file mode 100644
index 0000000..7f7a334
--- /dev/null
+++ b/src/components/atoms/links/nav-link.stories.tsx
@@ -0,0 +1,55 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import NavLinkComponent from './nav-link';
+
+/**
+ * NavLink - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Typography/Links',
+ component: NavLinkComponent,
+ argTypes: {
+ href: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link target.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'The link label.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ logo: {
+ control: {
+ type: null,
+ },
+ description: 'The link logo.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof NavLinkComponent>;
+
+const Template: ComponentStory<typeof NavLinkComponent> = (args) => (
+ <NavLinkComponent {...args} />
+);
+
+/**
+ * Links Stories - Nav Link
+ */
+export const NavLink = Template.bind({});
+NavLink.args = {
+ href: '#',
+ label: 'A nav link',
+};
diff --git a/src/components/atoms/links/nav-link.test.tsx b/src/components/atoms/links/nav-link.test.tsx
new file mode 100644
index 0000000..7750cee
--- /dev/null
+++ b/src/components/atoms/links/nav-link.test.tsx
@@ -0,0 +1,12 @@
+import { render, screen } from '@test-utils';
+import NavLink from './nav-link';
+
+describe('NavLink', () => {
+ it('renders a nav link to blog page', () => {
+ render(<NavLink href="/blog" label="Blog" />);
+ expect(screen.getByRole('link', { name: 'Blog' })).toHaveAttribute(
+ 'href',
+ '/blog'
+ );
+ });
+});
diff --git a/src/components/atoms/links/nav-link.tsx b/src/components/atoms/links/nav-link.tsx
new file mode 100644
index 0000000..7c6fede
--- /dev/null
+++ b/src/components/atoms/links/nav-link.tsx
@@ -0,0 +1,36 @@
+import Link from 'next/link';
+import { FC, ReactNode } from 'react';
+import styles from './nav-link.module.scss';
+
+export type NavLinkProps = {
+ /**
+ * Link target.
+ */
+ href: string;
+ /**
+ * Link label.
+ */
+ label: string;
+ /**
+ * Link logo.
+ */
+ logo?: ReactNode;
+};
+
+/**
+ * NavLink component
+ *
+ * Render a navigation link.
+ */
+const NavLink: FC<NavLinkProps> = ({ href, label, logo }) => {
+ return (
+ <Link href={href}>
+ <a className={styles.link}>
+ {logo}
+ {label}
+ </a>
+ </Link>
+ );
+};
+
+export default NavLink;
diff --git a/src/components/Widgets/Sharing/Sharing.module.scss b/src/components/atoms/links/sharing-link.module.scss
index ada3e2f..26ca737 100644
--- a/src/components/Widgets/Sharing/Sharing.module.scss
+++ b/src/components/atoms/links/sharing-link.module.scss
@@ -1,56 +1,30 @@
@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-@use "@styles/abstracts/placeholders";
-
-.list {
- @extend %flex-list;
-
- gap: var(--spacing-sm);
- padding: var(--spacing-2xs) 0 0 var(--spacing-2xs);
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- gap: var(--spacing-xs);
- width: min-content;
- }
- }
-}
.link {
- display: flex;
- flex-flow: row nowrap;
+ display: inline-flex;
align-items: center;
padding: var(--spacing-2xs) var(--spacing-xs);
border-radius: fun.convert-px(3);
- font-weight: 600;
- text-decoration: none;
- transition: all 0.3s ease-in-out 0s;
&:hover,
&:focus {
- color: hsl(0, 0%, 100%);
transform: translateX(#{fun.convert-px(-3)})
translateY(#{fun.convert-px(-3)});
}
&:active {
- color: hsl(0, 0%, 100%);
transform: translateX(#{fun.convert-px(2)}) translateY(#{fun.convert-px(2)});
-
- @include mix.motion("reduce") {
- transform: none;
- }
}
&::before {
+ content: "";
display: block;
+ width: fun.convert-px(30);
+ height: fun.convert-px(30);
background-repeat: no-repeat;
- content: "";
filter: drop-shadow(
#{fun.convert-px(1)} #{fun.convert-px(1)} #{fun.convert-px(1)} hsl(0, 0%, 0%)
);
- width: fun.convert-px(30);
- height: fun.convert-px(30);
}
&--diaspora {
@@ -181,13 +155,3 @@
}
}
}
-
-:global {
- [data-theme="dark"] {
- :local {
- .link {
- filter: brightness(0.85) contrast(1.1);
- }
- }
- }
-}
diff --git a/src/components/atoms/links/sharing-link.stories.tsx b/src/components/atoms/links/sharing-link.stories.tsx
new file mode 100644
index 0000000..e6bd11b
--- /dev/null
+++ b/src/components/atoms/links/sharing-link.stories.tsx
@@ -0,0 +1,98 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SharingLinkComponent from './sharing-link';
+
+/**
+ * SharingLink - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Buttons/Sharing',
+ component: SharingLinkComponent,
+ argTypes: {
+ medium: {
+ control: {
+ type: 'select',
+ },
+ description: 'The sharing medium.',
+ options: [
+ 'diaspora',
+ 'email',
+ 'facebook',
+ 'journal-du-hacker',
+ 'linkedin',
+ 'twitter',
+ ],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ url: {
+ control: {
+ type: 'text',
+ },
+ description: 'The sharing url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SharingLinkComponent>;
+
+const Template: ComponentStory<typeof SharingLinkComponent> = (args) => (
+ <SharingLinkComponent {...args} />
+);
+
+/**
+ * Sharing Link Stories - Diaspora
+ */
+export const Diaspora = Template.bind({});
+Diaspora.args = {
+ medium: 'diaspora',
+ url: '#',
+};
+
+/**
+ * Sharing Link Stories - Email
+ */
+export const Email = Template.bind({});
+Email.args = {
+ medium: 'email',
+ url: '#',
+};
+
+/**
+ * Sharing Link Stories - Facebook
+ */
+export const Facebook = Template.bind({});
+Facebook.args = {
+ medium: 'facebook',
+ url: '#',
+};
+
+/**
+ * Sharing Link Stories - Journal du Hacker
+ */
+export const JournalDuHacker = Template.bind({});
+JournalDuHacker.args = {
+ medium: 'journal-du-hacker',
+ url: '#',
+};
+
+/**
+ * Sharing Link Stories - LinkedIn
+ */
+export const LinkedIn = Template.bind({});
+LinkedIn.args = {
+ medium: 'linkedin',
+ url: '#',
+};
+
+/**
+ * Sharing Link Stories - Twitter
+ */
+export const Twitter = Template.bind({});
+Twitter.args = {
+ medium: 'twitter',
+ url: '#',
+};
diff --git a/src/components/atoms/links/sharing-link.test.tsx b/src/components/atoms/links/sharing-link.test.tsx
new file mode 100644
index 0000000..e4c849c
--- /dev/null
+++ b/src/components/atoms/links/sharing-link.test.tsx
@@ -0,0 +1,46 @@
+import { render, screen } from '@test-utils';
+import SharingLink from './sharing-link';
+
+describe('SharingLink', () => {
+ it('render a Diaspora sharing link', () => {
+ render(<SharingLink medium="diaspora" url="#" />);
+ expect(screen.getByRole('link', { name: 'Share on diaspora' })).toHaveClass(
+ 'link--diaspora'
+ );
+ });
+
+ it('render an Email sharing link', () => {
+ render(<SharingLink medium="email" url="#" />);
+ expect(screen.getByRole('link', { name: 'Share on email' })).toHaveClass(
+ 'link--email'
+ );
+ });
+
+ it('render a Facebook sharing link', () => {
+ render(<SharingLink medium="facebook" url="#" />);
+ expect(screen.getByRole('link', { name: 'Share on facebook' })).toHaveClass(
+ 'link--facebook'
+ );
+ });
+
+ it('render a Journal du Hacker sharing link', () => {
+ render(<SharingLink medium="journal-du-hacker" url="#" />);
+ expect(
+ screen.getByRole('link', { name: 'Share on journal-du-hacker' })
+ ).toHaveClass('link--journal-du-hacker');
+ });
+
+ it('render a LinkedIn sharing link', () => {
+ render(<SharingLink medium="linkedin" url="#" />);
+ expect(screen.getByRole('link', { name: 'Share on linkedin' })).toHaveClass(
+ 'link--linkedin'
+ );
+ });
+
+ it('render a Twitter sharing link', () => {
+ render(<SharingLink medium="twitter" url="#" />);
+ expect(screen.getByRole('link', { name: 'Share on twitter' })).toHaveClass(
+ 'link--twitter'
+ );
+ });
+});
diff --git a/src/components/atoms/links/sharing-link.tsx b/src/components/atoms/links/sharing-link.tsx
new file mode 100644
index 0000000..ca53ef9
--- /dev/null
+++ b/src/components/atoms/links/sharing-link.tsx
@@ -0,0 +1,48 @@
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './sharing-link.module.scss';
+
+export type SharingMedium =
+ | 'diaspora'
+ | 'email'
+ | 'facebook'
+ | 'journal-du-hacker'
+ | 'linkedin'
+ | 'twitter';
+
+export type SharingLinkProps = {
+ /**
+ * The sharing medium id.
+ */
+ medium: SharingMedium;
+ /**
+ * The sharing url.
+ */
+ url: string;
+};
+
+/**
+ * SharingLink component
+ *
+ * Render a sharing link.
+ */
+const SharingLink: FC<SharingLinkProps> = ({ medium, url }) => {
+ const intl = useIntl();
+ const text = intl.formatMessage(
+ {
+ defaultMessage: 'Share on {name}',
+ description: 'Sharing: share on social network text',
+ id: 'ureXFw',
+ },
+ { name: medium }
+ );
+ const mediumClass = `link--${medium}`;
+
+ return (
+ <a href={url} className={`${styles.link} ${styles[mediumClass]}`}>
+ <span className="screen-reader-text">{text}</span>
+ </a>
+ );
+};
+
+export default SharingLink;
diff --git a/src/components/Widgets/SocialMedia/SocialMedia.module.scss b/src/components/atoms/links/social-link.module.scss
index 2ef34bf..02fc61c 100644
--- a/src/components/Widgets/SocialMedia/SocialMedia.module.scss
+++ b/src/components/atoms/links/social-link.module.scss
@@ -1,20 +1,9 @@
@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/placeholders";
-
-.list {
- @extend %flex-list;
-
- flex: 0 0 100%;
- gap: var(--spacing-xs);
- align-items: center;
- padding: var(--spacing-2xs) 0 0 var(--spacing-2xs);
-}
.link {
- display: block;
- width: 3em;
- height: 3em;
- background: none;
+ display: flex;
+ width: var(--link-size, #{fun.convert-px(60)});
+ height: var(--link-size, #{fun.convert-px(60)});
box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
var(--color-shadow),
fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-1)
@@ -22,7 +11,7 @@
fun.convert-px(3) fun.convert-px(4) fun.convert-px(4) fun.convert-px(-3)
var(--color-shadow),
0 0 0 0 var(--color-shadow);
- transition: all 0.3s linear 0s;
+ transition: all 0.25s linear 0s;
&:hover,
&:focus {
@@ -48,12 +37,7 @@
}
}
-:global {
- [data-theme="dark"] {
- :local {
- .icon {
- filter: brightness(0.85) contrast(1.1);
- }
- }
- }
+.icon {
+ max-width: 100%;
+ max-height: 100%;
}
diff --git a/src/components/atoms/links/social-link.stories.tsx b/src/components/atoms/links/social-link.stories.tsx
new file mode 100644
index 0000000..977ae6b
--- /dev/null
+++ b/src/components/atoms/links/social-link.stories.tsx
@@ -0,0 +1,73 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SocialLink from './social-link';
+
+/**
+ * SocialLink - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Buttons/Social',
+ component: SocialLink,
+ argTypes: {
+ name: {
+ control: {
+ type: 'select',
+ },
+ description: 'Social website name.',
+ options: ['Github', 'Gitlab', 'LinkedIn', 'Twitter'],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ url: {
+ control: {
+ type: null,
+ },
+ description: 'Social profile url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SocialLink>;
+
+const Template: ComponentStory<typeof SocialLink> = (args) => (
+ <SocialLink {...args} />
+);
+
+/**
+ * Social Link Stories - Github
+ */
+export const Github = Template.bind({});
+Github.args = {
+ name: 'Github',
+ url: '#',
+};
+
+/**
+ * Social Link Stories - Gitlab
+ */
+export const Gitlab = Template.bind({});
+Gitlab.args = {
+ name: 'Gitlab',
+ url: '#',
+};
+
+/**
+ * Social Link Stories - LinkedIn
+ */
+export const LinkedIn = Template.bind({});
+LinkedIn.args = {
+ name: 'LinkedIn',
+ url: '#',
+};
+
+/**
+ * Social Link Stories - Twitter
+ */
+export const Twitter = Template.bind({});
+Twitter.args = {
+ name: 'Twitter',
+ url: '#',
+};
diff --git a/src/components/atoms/links/social-link.test.tsx b/src/components/atoms/links/social-link.test.tsx
new file mode 100644
index 0000000..f49fb5a
--- /dev/null
+++ b/src/components/atoms/links/social-link.test.tsx
@@ -0,0 +1,15 @@
+import { render, screen } from '@test-utils';
+import SocialLink from './social-link';
+
+/**
+ * Next.js mock images to use next/image component. So for now, I need to mock
+ * the svg files manually.
+ */
+jest.mock('@assets/images/social-media/github.svg', () => 'svg-file');
+
+describe('SocialLink', () => {
+ it('render a social link', () => {
+ render(<SocialLink name="Github" url="#" />);
+ expect(screen.getByRole('link')).toHaveAccessibleName('Github');
+ });
+});
diff --git a/src/components/atoms/links/social-link.tsx b/src/components/atoms/links/social-link.tsx
new file mode 100644
index 0000000..464bc60
--- /dev/null
+++ b/src/components/atoms/links/social-link.tsx
@@ -0,0 +1,53 @@
+import GithubIcon from '@assets/images/social-media/github.svg';
+import GitlabIcon from '@assets/images/social-media/gitlab.svg';
+import LinkedInIcon from '@assets/images/social-media/linkedin.svg';
+import TwitterIcon from '@assets/images/social-media/twitter.svg';
+import { FC } from 'react';
+import styles from './social-link.module.scss';
+
+export type SocialWebsite = 'Github' | 'Gitlab' | 'LinkedIn' | 'Twitter';
+
+export type SocialLinkProps = {
+ /**
+ * The social website name.
+ */
+ name: SocialWebsite;
+ /**
+ * The social profile url.
+ */
+ url: string;
+};
+
+/**
+ * SocialLink component
+ *
+ * Render a social icon link.
+ */
+const SocialLink: FC<SocialLinkProps> = ({ name, url }) => {
+ /**
+ * Retrieve a social link icon by id.
+ * @param {string} id - The social website id.
+ */
+ const getIcon = (id: string) => {
+ switch (id) {
+ case 'Github':
+ return <GithubIcon className={styles.icon} aria-hidden="true" />;
+ case 'Gitlab':
+ return <GitlabIcon className={styles.icon} aria-hidden="true" />;
+ case 'LinkedIn':
+ return <LinkedInIcon className={styles.icon} aria-hidden="true" />;
+ case 'Twitter':
+ return <TwitterIcon className={styles.icon} aria-hidden="true" />;
+ default:
+ break;
+ }
+ };
+
+ return (
+ <a href={url} className={styles.link} aria-label={name}>
+ {getIcon(name)}
+ </a>
+ );
+};
+
+export default SocialLink;
diff --git a/src/components/atoms/lists/description-list-item.module.scss b/src/components/atoms/lists/description-list-item.module.scss
new file mode 100644
index 0000000..aba90ce
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.module.scss
@@ -0,0 +1,40 @@
+.term {
+ color: var(--color-fg-light);
+ font-weight: 600;
+}
+
+.description {
+ margin: 0;
+ word-break: break-all;
+}
+
+.wrapper {
+ display: flex;
+ width: fit-content;
+
+ &--has-separator {
+ .description:not(:first-of-type) {
+ &::before {
+ content: "/\0000a0";
+ }
+ }
+ }
+
+ &--inline,
+ &--inline-values {
+ flex-flow: row wrap;
+ column-gap: var(--spacing-2xs);
+ }
+
+ &--inline-values {
+ row-gap: var(--spacing-2xs);
+
+ .term {
+ flex: 1 1 100%;
+ }
+ }
+
+ &--stacked {
+ flex-flow: column wrap;
+ }
+}
diff --git a/src/components/atoms/lists/description-list-item.stories.tsx b/src/components/atoms/lists/description-list-item.stories.tsx
new file mode 100644
index 0000000..c7beb0d
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.stories.tsx
@@ -0,0 +1,132 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import DescriptionListItemComponent from './description-list-item';
+
+export default {
+ title: 'Atoms/Typography/Lists/DescriptionList/Item',
+ component: DescriptionListItemComponent,
+ args: {
+ layout: 'stacked',
+ withSeparator: false,
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list item wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ descriptionClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list item description.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'The item label.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ layout: {
+ control: {
+ type: 'select',
+ },
+ description: 'The item layout.',
+ options: ['inline', 'inline-values', 'stacked'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'stacked' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ termClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list item term.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ value: {
+ description: 'The item value.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ withSeparator: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Add a slash as separator between multiple values.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof DescriptionListItemComponent>;
+
+const Template: ComponentStory<typeof DescriptionListItemComponent> = (
+ args
+) => <DescriptionListItemComponent {...args} />;
+
+export const SingleValueStacked = Template.bind({});
+SingleValueStacked.args = {
+ label: 'Recusandae vitae tenetur',
+ value: ['praesentium'],
+ layout: 'stacked',
+};
+
+export const SingleValueInlined = Template.bind({});
+SingleValueInlined.args = {
+ label: 'Recusandae vitae tenetur',
+ value: ['praesentium'],
+ layout: 'inline',
+};
+
+export const MultipleValuesStacked = Template.bind({});
+MultipleValuesStacked.args = {
+ label: 'Recusandae vitae tenetur',
+ value: ['praesentium', 'voluptate', 'tempore'],
+ layout: 'stacked',
+};
+
+export const MultipleValuesInlined = Template.bind({});
+MultipleValuesInlined.args = {
+ label: 'Recusandae vitae tenetur',
+ value: ['praesentium', 'voluptate', 'tempore'],
+ layout: 'inline-values',
+ withSeparator: true,
+};
diff --git a/src/components/atoms/lists/description-list-item.test.tsx b/src/components/atoms/lists/description-list-item.test.tsx
new file mode 100644
index 0000000..730a52f
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.test.tsx
@@ -0,0 +1,17 @@
+import { render, screen } from '@test-utils';
+import DescriptionListItem from './description-list-item';
+
+const itemLabel = 'Repellendus corporis facilis';
+const itemValue = ['quos', 'eum'];
+
+describe('DescriptionListItem', () => {
+ it('renders a couple of label', () => {
+ render(<DescriptionListItem label={itemLabel} value={itemValue} />);
+ expect(screen.getByRole('term')).toHaveTextContent(itemLabel);
+ });
+
+ it('renders the right number of values', () => {
+ render(<DescriptionListItem label={itemLabel} value={itemValue} />);
+ expect(screen.getAllByRole('definition')).toHaveLength(itemValue.length);
+ });
+});
diff --git a/src/components/atoms/lists/description-list-item.tsx b/src/components/atoms/lists/description-list-item.tsx
new file mode 100644
index 0000000..9505d01
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.tsx
@@ -0,0 +1,73 @@
+import { FC, ReactNode, useId } from 'react';
+import styles from './description-list-item.module.scss';
+
+export type ItemLayout = 'inline' | 'inline-values' | 'stacked';
+
+export type DescriptionListItemProps = {
+ /**
+ * Set additional classnames to the list item wrapper.
+ */
+ className?: string;
+ /**
+ * Set additional classnames to the list item description.
+ */
+ descriptionClassName?: string;
+ /**
+ * The item label.
+ */
+ label: string;
+ /**
+ * The item layout.
+ */
+ layout?: ItemLayout;
+ /**
+ * Set additional classnames to the list item term.
+ */
+ termClassName?: string;
+ /**
+ * The item value.
+ */
+ value: ReactNode | ReactNode[];
+ /**
+ * If true, use a slash to delimitate multiple values.
+ */
+ withSeparator?: boolean;
+};
+
+/**
+ * DescriptionListItem component
+ *
+ * Render a couple of dt/dd wrapped in a div.
+ */
+const DescriptionListItem: FC<DescriptionListItemProps> = ({
+ className = '',
+ descriptionClassName = '',
+ label,
+ termClassName = '',
+ value,
+ layout = 'stacked',
+ withSeparator = false,
+}) => {
+ const id = useId();
+ const layoutStyles = styles[`wrapper--${layout}`];
+ const separatorStyles = withSeparator ? styles['wrapper--has-separator'] : '';
+ const itemValues = Array.isArray(value) ? value : [value];
+
+ return (
+ <div
+ className={`${styles.wrapper} ${layoutStyles} ${separatorStyles} ${className}`}
+ >
+ <dt className={`${styles.term} ${termClassName}`}>{label}</dt>
+ {itemValues.map((currentValue, index) => (
+ <dd
+ key={`${id}-${index}`}
+ className={`${styles.description} ${descriptionClassName}`}
+ >
+ {currentValue}
+ </dd>
+ ))}
+ </div>
+ );
+};
+
+export default DescriptionListItem;
diff --git a/src/components/atoms/lists/description-list.module.scss b/src/components/atoms/lists/description-list.module.scss
new file mode 100644
index 0000000..9e913d4
--- /dev/null
+++ b/src/components/atoms/lists/description-list.module.scss
@@ -0,0 +1,17 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.list {
+ display: flex;
+ column-gap: var(--spacing-md);
+ row-gap: var(--spacing-2xs);
+ margin: 0;
+
+ &--inline {
+ flex-flow: row wrap;
+ align-items: baseline;
+ }
+
+ &--column {
+ flex-flow: column wrap;
+ }
+}
diff --git a/src/components/atoms/lists/description-list.stories.tsx b/src/components/atoms/lists/description-list.stories.tsx
new file mode 100644
index 0000000..347fd78
--- /dev/null
+++ b/src/components/atoms/lists/description-list.stories.tsx
@@ -0,0 +1,131 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import DescriptionList, { DescriptionListItem } from './description-list';
+
+/**
+ * DescriptionList - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Typography/Lists/DescriptionList',
+ component: DescriptionList,
+ args: {
+ layout: 'column',
+ withSeparator: false,
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list wrapper',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ groupClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the item wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ control: {
+ type: null,
+ },
+ description: 'The list items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ layout: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list layout.',
+ options: ['column', 'inline'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'column' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ valueClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the value wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ withSeparator: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Add a slash as separator between multiple values.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof DescriptionList>;
+
+const Template: ComponentStory<typeof DescriptionList> = (args) => (
+ <DescriptionList {...args} />
+);
+
+const items: DescriptionListItem[] = [
+ { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] },
+ { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] },
+ {
+ id: 'term-3',
+ label: 'Term 3:',
+ value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
+ },
+ { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] },
+];
+
+/**
+ * List Stories - Description list
+ */
+export const List = Template.bind({});
+List.args = {
+ items,
+};
diff --git a/src/components/atoms/lists/description-list.test.tsx b/src/components/atoms/lists/description-list.test.tsx
new file mode 100644
index 0000000..83e405f
--- /dev/null
+++ b/src/components/atoms/lists/description-list.test.tsx
@@ -0,0 +1,20 @@
+import { render } from '@test-utils';
+import DescriptionList, { DescriptionListItem } from './description-list';
+
+const items: DescriptionListItem[] = [
+ { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] },
+ { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] },
+ {
+ id: 'term-3',
+ label: 'Term 3:',
+ value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
+ },
+ { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] },
+];
+
+describe('DescriptionList', () => {
+ it('renders a list of terms and description', () => {
+ const { container } = render(<DescriptionList items={items} />);
+ expect(container).toBeDefined();
+ });
+});
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
new file mode 100644
index 0000000..a8e2d53
--- /dev/null
+++ b/src/components/atoms/lists/description-list.tsx
@@ -0,0 +1,103 @@
+import { FC } from 'react';
+import DescriptionListItem, {
+ type DescriptionListItemProps,
+} from './description-list-item';
+import styles from './description-list.module.scss';
+
+export type DescriptionListItem = {
+ /**
+ * The item id.
+ */
+ id: string;
+ /**
+ * The list item layout.
+ */
+ layout?: DescriptionListItemProps['layout'];
+ /**
+ * A list label.
+ */
+ label: DescriptionListItemProps['label'];
+ /**
+ * An array of values for the list item.
+ */
+ value: DescriptionListItemProps['value'];
+};
+
+export type DescriptionListProps = {
+ /**
+ * Set additional classnames to the list wrapper.
+ */
+ className?: string;
+ /**
+ * Set additional classnames to the `dt`/`dd` couple wrapper.
+ */
+ groupClassName?: string;
+ /**
+ * The list items.
+ */
+ items: DescriptionListItem[];
+ /**
+ * Set additional classnames to the `dt` element.
+ */
+ labelClassName?: string;
+ /**
+ * The list layout. Default: column.
+ */
+ layout?: 'inline' | 'column';
+ /**
+ * Set additional classnames to the `dd` element.
+ */
+ valueClassName?: string;
+ /**
+ * If true, use a slash to delimitate multiple values.
+ */
+ withSeparator?: DescriptionListItemProps['withSeparator'];
+};
+
+/**
+ * DescriptionList component
+ *
+ * Render a description list.
+ */
+const DescriptionList: FC<DescriptionListProps> = ({
+ className = '',
+ groupClassName = '',
+ items,
+ labelClassName = '',
+ layout = 'column',
+ valueClassName = '',
+ withSeparator,
+}) => {
+ const layoutModifier = `list--${layout}`;
+
+ /**
+ * Retrieve the description list items.
+ *
+ * @param {DescriptionListItem[]} listItems - An array of items.
+ * @returns {JSX.Element[]} The description list items.
+ */
+ const getItems = (listItems: DescriptionListItem[]): JSX.Element[] => {
+ return listItems.map(({ id, layout: itemLayout, label, value }) => {
+ return (
+ <DescriptionListItem
+ key={id}
+ label={label}
+ value={value}
+ layout={itemLayout}
+ className={groupClassName}
+ descriptionClassName={valueClassName}
+ termClassName={labelClassName}
+ withSeparator={withSeparator}
+ />
+ );
+ });
+ };
+
+ return (
+ <dl className={`${styles.list} ${styles[layoutModifier]} ${className}`}>
+ {getItems(items)}
+ </dl>
+ );
+};
+
+export default DescriptionList;
diff --git a/src/components/atoms/lists/list.module.scss b/src/components/atoms/lists/list.module.scss
new file mode 100644
index 0000000..95f9b40
--- /dev/null
+++ b/src/components/atoms/lists/list.module.scss
@@ -0,0 +1,45 @@
+@use "@styles/abstracts/placeholders";
+
+.list {
+ margin: 0;
+
+ ::marker {
+ color: var(--color-primary-dark);
+ }
+
+ &--ordered {
+ padding: 0;
+ counter-reset: li;
+ list-style-type: none;
+ }
+
+ &--ordered &__item {
+ display: table;
+ counter-increment: li;
+
+ &::before {
+ content: counters(li, ".") ". ";
+ display: table-cell;
+ padding-right: var(--spacing-2xs);
+ color: var(--color-secondary);
+ }
+ }
+
+ &--unordered {
+ padding: 0 0 0 var(--spacing-sm);
+ }
+
+ &--flex {
+ @extend %reset-list;
+
+ display: flex;
+ flex-flow: row wrap;
+ gap: var(--spacing-sm);
+ }
+
+ &--flex &--flex {
+ display: initial;
+ position: relative;
+ top: var(--spacing-2xs);
+ }
+}
diff --git a/src/components/atoms/lists/list.stories.tsx b/src/components/atoms/lists/list.stories.tsx
new file mode 100644
index 0000000..eac3cd3
--- /dev/null
+++ b/src/components/atoms/lists/list.stories.tsx
@@ -0,0 +1,111 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ListComponent, { type ListItem } from './list';
+
+/**
+ * List - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Typography/Lists',
+ component: ListComponent,
+ args: {
+ kind: 'unordered',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list wrapper',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ control: {
+ type: null,
+ },
+ description: 'The list items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ itemsClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list items.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list kind: flex, ordered or unordered.',
+ options: ['flex', 'ordered', 'unordered'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'unordered' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ListComponent>;
+
+const Template: ComponentStory<typeof ListComponent> = (args) => (
+ <ListComponent {...args} />
+);
+
+const items: ListItem[] = [
+ { id: 'item-1', value: 'Item 1' },
+ { id: 'item-2', value: 'Item 2' },
+ {
+ child: [
+ { id: 'nested-item-1', value: 'Nested item 1' },
+ { id: 'nested-item-2', value: 'Nested item 2' },
+ ],
+ id: 'item-3',
+ value: 'Item 3',
+ },
+ { id: 'item-4', value: 'Item 4' },
+];
+
+/**
+ * List Stories - Flex list
+ */
+export const Flex = Template.bind({});
+Flex.args = {
+ items,
+ kind: 'flex',
+};
+
+/**
+ * List Stories - Ordered list
+ */
+export const Ordered = Template.bind({});
+Ordered.args = {
+ items,
+ kind: 'ordered',
+};
+
+/**
+ * List Stories - Unordered list
+ */
+export const Unordered = Template.bind({});
+Unordered.args = {
+ items,
+};
diff --git a/src/components/atoms/lists/list.test.tsx b/src/components/atoms/lists/list.test.tsx
new file mode 100644
index 0000000..fcf8813
--- /dev/null
+++ b/src/components/atoms/lists/list.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from '@test-utils';
+import List, { type ListItem } from './list';
+
+const items: ListItem[] = [
+ { id: 'item-1', value: 'Item 1' },
+ { id: 'item-2', value: 'Item 2' },
+ {
+ child: [
+ { id: 'nested-item-1', value: 'Nested item 1' },
+ { id: 'nested-item-2', value: 'Nested item 2' },
+ ],
+ id: 'item-3',
+ value: 'Item 3',
+ },
+ { id: 'item-4', value: 'Item 4' },
+];
+
+describe('List', () => {
+ it('renders a nested unordered list', () => {
+ render(<List items={items} />);
+ const listItems = screen.getAllByRole('list');
+ listItems.forEach((listItem) =>
+ expect(listItem).toHaveClass('list--unordered')
+ );
+ });
+});
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
new file mode 100644
index 0000000..aa0a241
--- /dev/null
+++ b/src/components/atoms/lists/list.tsx
@@ -0,0 +1,79 @@
+import { FC } from 'react';
+import styles from './list.module.scss';
+
+export type ListItem = {
+ /**
+ * Nested list.
+ */
+ child?: ListItem[];
+ /**
+ * Item id.
+ */
+ id: string;
+ /**
+ * Item value.
+ */
+ value: any;
+};
+
+export type ListProps = {
+ /**
+ * Set additional classnames to the list wrapper.
+ */
+ className?: string;
+ /**
+ * An array of list items.
+ */
+ items: ListItem[];
+ /**
+ * Set additional classnames to the list items.
+ */
+ itemsClassName?: string;
+ /**
+ * The list kind.
+ */
+ kind?: 'ordered' | 'unordered' | 'flex';
+};
+
+/**
+ * List component
+ *
+ * Render either an ordered or an unordered list.
+ */
+const List: FC<ListProps> = ({
+ className = '',
+ items,
+ itemsClassName = '',
+ kind = 'unordered',
+}) => {
+ const ListTag = kind === 'ordered' ? 'ol' : 'ul';
+ const kindClass = `list--${kind}`;
+
+ /**
+ * Retrieve the list items.
+ * @param array - An array of items.
+ * @returns {JSX.Element[]} - An array of li elements.
+ */
+ const getItems = (array: ListItem[]): JSX.Element[] => {
+ return array.map(({ child, id, value }) => (
+ <li key={id} className={`${styles.list__item} ${itemsClassName}`}>
+ {value}
+ {child && (
+ <ListTag
+ className={`${styles.list} ${styles[kindClass]} ${className}`}
+ >
+ {getItems(child)}
+ </ListTag>
+ )}
+ </li>
+ ));
+ };
+
+ return (
+ <ListTag className={`${styles.list} ${styles[kindClass]} ${className}`}>
+ {getItems(items)}
+ </ListTag>
+ );
+};
+
+export default List;
diff --git a/src/components/PaginationCursor/PaginationCursor.module.scss b/src/components/atoms/loaders/progress-bar.module.scss
index 542584c..878010a 100644
--- a/src/components/PaginationCursor/PaginationCursor.module.scss
+++ b/src/components/atoms/loaders/progress-bar.module.scss
@@ -1,15 +1,20 @@
@use "@styles/abstracts/functions" as fun;
-.wrapper {
- width: max-content;
+.progress {
margin: var(--spacing-sm) auto var(--spacing-md);
text-align: center;
- .bar[value] {
+ &__info {
+ margin-bottom: var(--spacing-2xs);
+ font-size: var(--font-size-sm);
+ }
+
+ &__bar[value] {
display: block;
width: clamp(25ch, 20vw, 30ch);
max-width: 100%;
height: fun.convert-px(13);
+ margin: auto;
appearance: none;
background: var(--color-bg-tertiary);
border: fun.convert-px(1) solid var(--color-primary-darker);
@@ -36,8 +41,3 @@
}
}
}
-
-.info {
- margin-bottom: var(--spacing-2xs);
- font-size: var(--font-size-sm);
-}
diff --git a/src/components/atoms/loaders/progress-bar.stories.tsx b/src/components/atoms/loaders/progress-bar.stories.tsx
new file mode 100644
index 0000000..fcd631c
--- /dev/null
+++ b/src/components/atoms/loaders/progress-bar.stories.tsx
@@ -0,0 +1,93 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ProgressBarComponent from './progress-bar';
+
+/**
+ * ProgressBar - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Loaders/ProgressBar',
+ component: ProgressBarComponent,
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'string',
+ },
+ description: 'An accessible name.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ current: {
+ control: {
+ type: 'number',
+ },
+ description: 'The current value.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ info: {
+ control: {
+ type: 'text',
+ },
+ description: 'An additional information to display.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ max: {
+ control: {
+ type: 'number',
+ },
+ description: 'The maximal value.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ min: {
+ control: {
+ type: 'number',
+ },
+ description: 'The minimal value.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof ProgressBarComponent>;
+
+const Template: ComponentStory<typeof ProgressBarComponent> = (args) => (
+ <ProgressBarComponent {...args} />
+);
+
+/**
+ * Loaders Stories - Default Progress bar
+ */
+export const ProgressBar = Template.bind({});
+ProgressBar.args = {
+ current: 10,
+ min: 0,
+ max: 50,
+};
+
+/**
+ * Loaders Stories - Progress bar With Info
+ */
+export const ProgressBarWithInfo = Template.bind({});
+ProgressBarWithInfo.args = {
+ current: 10,
+ info: 'Loaded: 10 / 50',
+ min: 0,
+ max: 50,
+};
diff --git a/src/components/atoms/loaders/progress-bar.test.tsx b/src/components/atoms/loaders/progress-bar.test.tsx
new file mode 100644
index 0000000..37a7364
--- /dev/null
+++ b/src/components/atoms/loaders/progress-bar.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import ProgressBar from './progress-bar';
+
+describe('ProgressBar', () => {
+ it('renders a progress bar', () => {
+ render(<ProgressBar min={0} max={50} current={10} />);
+ expect(screen.getByRole('progressbar')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/atoms/loaders/progress-bar.tsx b/src/components/atoms/loaders/progress-bar.tsx
new file mode 100644
index 0000000..9bac847
--- /dev/null
+++ b/src/components/atoms/loaders/progress-bar.tsx
@@ -0,0 +1,55 @@
+import { FC } from 'react';
+import styles from './progress-bar.module.scss';
+
+export type ProgressBarProps = {
+ /**
+ * Accessible progress bar name.
+ */
+ 'aria-label'?: string;
+ /**
+ * Current value.
+ */
+ current: number;
+ /**
+ * Additional information to display before progress bar.
+ */
+ info?: string;
+ /**
+ * Minimal value.
+ */
+ min: number;
+ /**
+ * Maximal value.
+ */
+ max: number;
+};
+
+/**
+ * ProgressBar component
+ *
+ * Render a progress bar.
+ */
+const ProgressBar: FC<ProgressBarProps> = ({
+ current,
+ info,
+ min,
+ max,
+ ...props
+}) => {
+ return (
+ <div className={styles.progress}>
+ {info && <div className={styles.progress__info}>{info}</div>}
+ <progress
+ className={styles.progress__bar}
+ max={max}
+ value={current}
+ aria-valuemin={min}
+ aria-valuemax={max}
+ aria-valuenow={current}
+ {...props}
+ ></progress>
+ </div>
+ );
+};
+
+export default ProgressBar;
diff --git a/src/components/Spinner/Spinner.module.scss b/src/components/atoms/loaders/spinner.module.scss
index 8d818a2..8d818a2 100644
--- a/src/components/Spinner/Spinner.module.scss
+++ b/src/components/atoms/loaders/spinner.module.scss
diff --git a/src/components/atoms/loaders/spinner.stories.tsx b/src/components/atoms/loaders/spinner.stories.tsx
new file mode 100644
index 0000000..1792c6c
--- /dev/null
+++ b/src/components/atoms/loaders/spinner.stories.tsx
@@ -0,0 +1,42 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SpinnerComponent from './spinner';
+
+/**
+ * Spinner - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Loaders/Spinner',
+ component: SpinnerComponent,
+ argTypes: {
+ message: {
+ control: {
+ type: 'text',
+ },
+ description: 'Loading message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof SpinnerComponent>;
+
+const Template: ComponentStory<typeof SpinnerComponent> = (args) => (
+ <SpinnerComponent {...args} />
+);
+
+/**
+ * Loaders Stories - Default Spinner
+ */
+export const Spinner = Template.bind({});
+
+/**
+ * Loaders Stories - Spinner with custom message
+ */
+export const SpinnerCustomMessage = Template.bind({});
+SpinnerCustomMessage.args = {
+ message: 'Submitting...',
+};
diff --git a/src/components/atoms/loaders/spinner.test.tsx b/src/components/atoms/loaders/spinner.test.tsx
new file mode 100644
index 0000000..0a6db91
--- /dev/null
+++ b/src/components/atoms/loaders/spinner.test.tsx
@@ -0,0 +1,14 @@
+import { render, screen } from '@test-utils';
+import Spinner from './spinner';
+
+describe('Spinner', () => {
+ it('renders a spinner loader', () => {
+ render(<Spinner />);
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
+ });
+
+ it('renders a spinner loader with a custom message', () => {
+ render(<Spinner message="Submitting" />);
+ expect(screen.getByText('Submitting')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/Spinner/Spinner.tsx b/src/components/atoms/loaders/spinner.tsx
index 9117d90..6655141 100644
--- a/src/components/Spinner/Spinner.tsx
+++ b/src/components/atoms/loaders/spinner.tsx
@@ -1,7 +1,20 @@
+import { FC } from 'react';
import { useIntl } from 'react-intl';
-import styles from './Spinner.module.scss';
+import styles from './spinner.module.scss';
-const Spinner = ({ message }: { message?: string }) => {
+export type SpinnerProps = {
+ /**
+ * The loading message. Default: "Loading...".
+ */
+ message?: string;
+};
+
+/**
+ * Spinner component
+ *
+ * Render a loading message with animation.
+ */
+const Spinner: FC<SpinnerProps> = ({ message }) => {
const intl = useIntl();
return (
diff --git a/src/components/molecules/buttons/back-to-top.module.scss b/src/components/molecules/buttons/back-to-top.module.scss
new file mode 100644
index 0000000..77ee97b
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.module.scss
@@ -0,0 +1,51 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ .link {
+ width: clamp(#{fun.convert-px(48)}, 8vw, #{fun.convert-px(55)});
+ height: clamp(#{fun.convert-px(48)}, 8vw, #{fun.convert-px(55)});
+
+ svg {
+ width: 100%;
+ }
+
+ :global {
+ .arrow-head {
+ transform: translateY(30%) scale(1.2);
+ transition: all 0.45s ease-in-out 0s;
+ }
+
+ .arrow-bar {
+ opacity: 0;
+ transform: translateY(30%) scaleY(0);
+ transition: transform 0.45s ease-in-out 0s, opacity 0.1s linear 0.2s;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ :global {
+ .arrow-head {
+ transform: translateY(0) scale(1);
+ }
+
+ .arrow-bar {
+ opacity: 1;
+ transform: translateY(0) scaleY(1);
+ }
+ }
+
+ svg {
+ :global {
+ animation: pulse 1.2s ease-in-out 0.6s infinite;
+ }
+ }
+ }
+
+ &:active {
+ svg {
+ animation-play-state: paused;
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/buttons/back-to-top.stories.tsx b/src/components/molecules/buttons/back-to-top.stories.tsx
new file mode 100644
index 0000000..a338b8b
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import BackToTopComponent from './back-to-top';
+
+/**
+ * BackToTop - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Buttons',
+ component: BackToTopComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ target: {
+ control: {
+ type: 'text',
+ },
+ description: 'An element id (without hashtag) to use as anchor.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof BackToTopComponent>;
+
+const Template: ComponentStory<typeof BackToTopComponent> = (args) => (
+ <BackToTopComponent {...args} />
+);
+
+/**
+ * Buttons Stories - Back to top
+ */
+export const BackToTop = Template.bind({});
+BackToTop.args = {
+ target: 'top',
+};
diff --git a/src/components/molecules/buttons/back-to-top.test.tsx b/src/components/molecules/buttons/back-to-top.test.tsx
new file mode 100644
index 0000000..2b3a0a9
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.test.tsx
@@ -0,0 +1,10 @@
+import { render, screen } from '@test-utils';
+import BackToTop from './back-to-top';
+
+describe('BackToTop', () => {
+ it('renders a BackToTop link', () => {
+ render(<BackToTop target="top" />);
+ expect(screen.getByRole('link')).toHaveAccessibleName('Back to top');
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/#top');
+ });
+});
diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx
new file mode 100644
index 0000000..bd1925a
--- /dev/null
+++ b/src/components/molecules/buttons/back-to-top.tsx
@@ -0,0 +1,43 @@
+import ButtonLink, {
+ type ButtonLinkProps,
+} from '@components/atoms/buttons/button-link';
+import Arrow from '@components/atoms/icons/arrow';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './back-to-top.module.scss';
+
+export type BackToTopProps = Pick<ButtonLinkProps, 'target'> & {
+ /**
+ * Set additional classnames to the button wrapper.
+ */
+ className?: string;
+};
+
+/**
+ * BackToTop component
+ *
+ * Render a back to top link.
+ */
+const BackToTop: FC<BackToTopProps> = ({ className = '', target }) => {
+ const intl = useIntl();
+ const linkName = intl.formatMessage({
+ defaultMessage: 'Back to top',
+ description: 'BackToTop: link text',
+ id: 'm+SUSR',
+ });
+
+ return (
+ <div className={`${styles.wrapper} ${className}`}>
+ <ButtonLink
+ shape="square"
+ target={`#${target}`}
+ aria-label={linkName}
+ className={styles.link}
+ >
+ <Arrow direction="top" />
+ </ButtonLink>
+ </div>
+ );
+};
+
+export default BackToTop;
diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss
new file mode 100644
index 0000000..3c69221
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.module.scss
@@ -0,0 +1,44 @@
+@use "@styles/abstracts/functions" as fun;
+
+.icon {
+ transition: all 0.25s ease-in-out 0s;
+}
+
+.wrapper {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--spacing-md);
+ width: 100%;
+ padding: 0 var(--spacing-2xs);
+ position: sticky;
+ top: 0;
+ background: inherit;
+ border: none;
+ border-top: fun.convert-px(2) solid var(--color-primary-dark);
+ border-bottom: fun.convert-px(2) solid var(--color-primary-dark);
+ cursor: pointer;
+
+ .heading {
+ padding: var(--spacing-2xs) 0;
+ background: none;
+ font-size: var(--font-size-xl);
+ font-weight: 500;
+ text-align: left;
+ }
+
+ &:hover,
+ &:focus {
+ .icon {
+ background: var(--color-primary-light);
+ color: var(--color-fg-inverted);
+ transform: scale(1.25);
+
+ &::before,
+ &::after {
+ background: var(--color-bg);
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/buttons/heading-button.stories.tsx b/src/components/molecules/buttons/heading-button.stories.tsx
new file mode 100644
index 0000000..59f7be9
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.stories.tsx
@@ -0,0 +1,105 @@
+import headingStories from '@components/atoms/headings/heading.stories';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import HeadingButtonComponent from './heading-button';
+
+/**
+ * HeadingButton - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Buttons/HeadingButton',
+ component: HeadingButtonComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ expanded: {
+ control: {
+ type: null,
+ },
+ description: 'Heading button state (plus or minus).',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ level: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'Heading level.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ setExpanded: {
+ control: {
+ type: null,
+ },
+ description: 'Callback function to set heading button state.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'Heading title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof HeadingButtonComponent>;
+
+const Template: ComponentStory<typeof HeadingButtonComponent> = ({
+ expanded,
+ setExpanded: _setExpanded,
+ ...args
+}) => {
+ const [isExpanded, setIsExpanded] = useState<boolean>(expanded);
+
+ return (
+ <HeadingButtonComponent
+ expanded={isExpanded}
+ setExpanded={setIsExpanded}
+ {...args}
+ />
+ );
+};
+
+/**
+ * Heading Button Stories - Expanded
+ */
+export const Expanded = Template.bind({});
+Expanded.args = {
+ expanded: true,
+ level: 2,
+ title: 'Your title',
+};
+
+/**
+ * Heading Button Stories - Collapsed
+ */
+export const Collapsed = Template.bind({});
+Collapsed.args = {
+ expanded: false,
+ level: 2,
+ title: 'Your title',
+};
diff --git a/src/components/molecules/buttons/heading-button.test.tsx b/src/components/molecules/buttons/heading-button.test.tsx
new file mode 100644
index 0000000..be3865a
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen } from '@test-utils';
+import HeadingButton from './heading-button';
+
+describe('HeadingButton', () => {
+ it('renders a button to collapse.', () => {
+ render(
+ <HeadingButton
+ level={2}
+ title="The accordion title"
+ expanded={true}
+ setExpanded={() => null}
+ />
+ );
+ expect(
+ screen.getByRole('button', { name: 'Collapse The accordion title' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a button to expand.', () => {
+ render(
+ <HeadingButton
+ level={2}
+ title="The accordion title"
+ expanded={false}
+ setExpanded={() => null}
+ />
+ );
+ expect(
+ screen.getByRole('button', { name: 'Expand The accordion title' })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx
new file mode 100644
index 0000000..0ed9a76
--- /dev/null
+++ b/src/components/molecules/buttons/heading-button.tsx
@@ -0,0 +1,67 @@
+import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
+import PlusMinus from '@components/atoms/icons/plus-minus';
+import { FC, SetStateAction } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './heading-button.module.scss';
+
+export type HeadingButtonProps = Pick<HeadingProps, 'level'> & {
+ /**
+ * Set additional classnames to the button.
+ */
+ className?: string;
+ /**
+ * Accordion state.
+ */
+ expanded: boolean;
+ /**
+ * Callback function to set accordion state on click.
+ */
+ setExpanded: (value: SetStateAction<boolean>) => void;
+ /**
+ * Accordion title.
+ */
+ title: string;
+};
+
+/**
+ * HeadingButton component
+ *
+ * Render a button as accordion title to toggle body.
+ */
+const HeadingButton: FC<HeadingButtonProps> = ({
+ className = '',
+ expanded,
+ level,
+ setExpanded,
+ title,
+}) => {
+ const intl = useIntl();
+ const iconState = expanded ? 'minus' : 'plus';
+ const titlePrefix = expanded
+ ? intl.formatMessage({
+ defaultMessage: 'Collapse',
+ description: 'HeadingButton: title prefix (expanded state)',
+ id: 'UX9Bu8',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'Expand',
+ description: 'HeadingButton: title prefix (collapsed state)',
+ id: 'bcyOgC',
+ });
+
+ return (
+ <button
+ type="button"
+ className={`${styles.wrapper} ${className}`}
+ onClick={() => setExpanded(!expanded)}
+ >
+ <Heading level={level} withMargin={false} className={styles.heading}>
+ <span className="screen-reader-text">{titlePrefix} </span>
+ {title}
+ </Heading>
+ <PlusMinus state={iconState} className={styles.icon} />
+ </button>
+ );
+};
+
+export default HeadingButton;
diff --git a/src/components/molecules/buttons/help-button.module.scss b/src/components/molecules/buttons/help-button.module.scss
new file mode 100644
index 0000000..42d49f6
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.module.scss
@@ -0,0 +1,21 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.btn {
+ padding: var(--spacing-xs);
+
+ &:not(:disabled) {
+ &:focus {
+ text-decoration: none;
+ }
+ }
+
+ @include mix.pointer("fine") {
+ padding: var(--spacing-2xs);
+ }
+}
+
+.icon {
+ color: var(--color-primary-dark);
+ font-weight: 600;
+}
diff --git a/src/components/molecules/buttons/help-button.stories.tsx b/src/components/molecules/buttons/help-button.stories.tsx
new file mode 100644
index 0000000..4968b27
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import HelpButtonComponent from './help-button';
+
+/**
+ * HelpButton - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Buttons',
+ component: HelpButtonComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the button wrapper.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ onClick: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to handle click on button.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof HelpButtonComponent>;
+
+const Template: ComponentStory<typeof HelpButtonComponent> = (args) => (
+ <HelpButtonComponent {...args} />
+);
+
+/**
+ * Help Button Stories - Level 1
+ */
+export const HelpButton = Template.bind({});
diff --git a/src/components/molecules/buttons/help-button.test.tsx b/src/components/molecules/buttons/help-button.test.tsx
new file mode 100644
index 0000000..78987ef
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import HelpButton from './help-button';
+
+describe('Help', () => {
+ it('renders a help button', () => {
+ render(<HelpButton />);
+ expect(screen.getByRole('button', { name: 'Help ?' })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
new file mode 100644
index 0000000..ccf1ebd
--- /dev/null
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -0,0 +1,37 @@
+import Button, { type ButtonProps } from '@components/atoms/buttons/button';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './help-button.module.scss';
+
+export type HelpButtonProps = Pick<ButtonProps, 'className' | 'onClick'>;
+
+/**
+ * HelpButton component
+ *
+ * Render a button with an interrogation mark icon.
+ */
+const HelpButton: ForwardRefRenderFunction<
+ HTMLButtonElement,
+ HelpButtonProps
+> = ({ className = '', ...props }, ref) => {
+ const intl = useIntl();
+ const text = intl.formatMessage({
+ defaultMessage: 'Help',
+ id: 'i+/ckF',
+ description: 'HelpButton: screen reader text',
+ });
+
+ return (
+ <Button
+ className={`${styles.btn} ${className}`}
+ ref={ref}
+ shape="circle"
+ {...props}
+ >
+ <span className="screen-reader-text">{text}</span>
+ <span className={styles.icon}>?</span>
+ </Button>
+ );
+};
+
+export default forwardRef(HelpButton);
diff --git a/src/components/Settings/AckeeSelect/AckeeSelect.module.scss b/src/components/molecules/forms/ackee-select.module.scss
index b145761..87cd9ee 100644
--- a/src/components/Settings/AckeeSelect/AckeeSelect.module.scss
+++ b/src/components/molecules/forms/ackee-select.module.scss
@@ -2,5 +2,10 @@
display: flex;
flex-flow: row wrap;
align-items: center;
- gap: var(--spacing-xs);
+ position: relative;
+}
+
+.tooltip {
+ position: absolute;
+ bottom: -100%;
}
diff --git a/src/components/molecules/forms/ackee-select.stories.tsx b/src/components/molecules/forms/ackee-select.stories.tsx
new file mode 100644
index 0000000..6b42b71
--- /dev/null
+++ b/src/components/molecules/forms/ackee-select.stories.tsx
@@ -0,0 +1,71 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import AckeeSelect from './ackee-select';
+
+/**
+ * AckeeSelect - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Select',
+ component: AckeeSelect,
+ argTypes: {
+ initialValue: {
+ control: {
+ type: 'select',
+ },
+ description: 'Initial selected option.',
+ options: ['full', 'partial'],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ storageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set Ackee settings local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ tooltipClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof AckeeSelect>;
+
+const Template: ComponentStory<typeof AckeeSelect> = (args) => (
+ <AckeeSelect {...args} />
+);
+
+/**
+ * Select Stories - Ackee select
+ */
+export const Ackee = Template.bind({});
+Ackee.args = {
+ initialValue: 'full',
+};
diff --git a/src/components/molecules/forms/ackee-select.test.tsx b/src/components/molecules/forms/ackee-select.test.tsx
new file mode 100644
index 0000000..0089c06
--- /dev/null
+++ b/src/components/molecules/forms/ackee-select.test.tsx
@@ -0,0 +1,25 @@
+import user from '@testing-library/user-event';
+import { act, render, screen } from '@test-utils';
+import AckeeSelect from './ackee-select';
+
+describe('Select', () => {
+ it('should correctly set default option', () => {
+ render(<AckeeSelect storageKey="ackee-tracking" initialValue="full" />);
+ expect(screen.getByRole('combobox')).toHaveValue('full');
+ expect(screen.queryByRole('combobox')).not.toHaveValue('partial');
+ });
+
+ it('should correctly change value when user choose another option', async () => {
+ render(<AckeeSelect storageKey="ackee-tracking" initialValue="full" />);
+
+ await act(async () => {
+ await user.selectOptions(
+ screen.getByRole('combobox'),
+ screen.getByRole('option', { name: 'Partial' })
+ );
+ });
+
+ expect(screen.getByRole('combobox')).toHaveValue('partial');
+ expect(screen.queryByRole('combobox')).not.toHaveValue('full');
+ });
+});
diff --git a/src/components/molecules/forms/ackee-select.tsx b/src/components/molecules/forms/ackee-select.tsx
new file mode 100644
index 0000000..34850fb
--- /dev/null
+++ b/src/components/molecules/forms/ackee-select.tsx
@@ -0,0 +1,103 @@
+import { type SelectOptions } from '@components/atoms/forms/select';
+import useLocalStorage from '@utils/hooks/use-local-storage';
+import useUpdateAckeeOptions, {
+ type AckeeOptions,
+} from '@utils/hooks/use-update-ackee-options';
+import { Dispatch, FC, SetStateAction } from 'react';
+import { useIntl } from 'react-intl';
+import SelectWithTooltip, {
+ type SelectWithTooltipProps,
+} from './select-with-tooltip';
+
+export type AckeeSelectProps = Pick<
+ SelectWithTooltipProps,
+ 'labelClassName' | 'tooltipClassName'
+> & {
+ /**
+ * A default value for Ackee settings.
+ */
+ initialValue: AckeeOptions;
+ /**
+ * The local storage key to save preference.
+ */
+ storageKey: string;
+};
+
+/**
+ * AckeeSelect component
+ *
+ * Render a select to set Ackee settings.
+ */
+const AckeeSelect: FC<AckeeSelectProps> = ({
+ initialValue,
+ storageKey,
+ ...props
+}) => {
+ const intl = useIntl();
+ const { value, setValue } = useLocalStorage<AckeeOptions>(
+ storageKey,
+ initialValue
+ );
+ useUpdateAckeeOptions(value);
+
+ const ackeeLabel = intl.formatMessage({
+ defaultMessage: 'Tracking:',
+ description: 'AckeeSelect: select label',
+ id: '2pmylc',
+ });
+ const tooltipTitle = intl.formatMessage({
+ defaultMessage: 'Ackee tracking (analytics)',
+ description: 'AckeeSelect: tooltip title',
+ id: 'F1EQX3',
+ });
+ const tooltipContent = [
+ intl.formatMessage({
+ defaultMessage: 'Partial includes only page url, views and duration.',
+ description: 'AckeeSelect: tooltip message',
+ id: 'skb4W5',
+ }),
+ intl.formatMessage({
+ defaultMessage:
+ 'Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.',
+ description: 'AckeeSelect: tooltip message',
+ id: 'Ogccx6',
+ }),
+ ];
+ const options: SelectOptions[] = [
+ {
+ id: 'partial',
+ name: intl.formatMessage({
+ defaultMessage: 'Partial',
+ description: 'AckeeSelect: partial option name',
+ id: 'e/8Kyj',
+ }),
+ value: 'partial',
+ },
+ {
+ id: 'full',
+ name: intl.formatMessage({
+ defaultMessage: 'Full',
+ description: 'AckeeSelect: full option name',
+ id: 'PzRpPw',
+ }),
+ value: 'full',
+ },
+ ];
+
+ return (
+ <SelectWithTooltip
+ id="ackee-settings"
+ name="ackee-settings"
+ label={ackeeLabel}
+ labelSize="medium"
+ options={options}
+ title={tooltipTitle}
+ content={tooltipContent}
+ value={value}
+ setValue={setValue as Dispatch<SetStateAction<string>>}
+ {...props}
+ />
+ );
+};
+
+export default AckeeSelect;
diff --git a/src/components/molecules/forms/flipping-label.module.scss b/src/components/molecules/forms/flipping-label.module.scss
new file mode 100644
index 0000000..e650ebe
--- /dev/null
+++ b/src/components/molecules/forms/flipping-label.module.scss
@@ -0,0 +1,63 @@
+@use "@styles/abstracts/functions" as fun;
+
+.label {
+ display: block;
+ width: var(--btn-size, #{fun.convert-px(60)});
+ height: var(--btn-size, #{fun.convert-px(60)});
+}
+
+.front,
+.back {
+ display: flex;
+ place-content: center;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ right: 0;
+ backface-visibility: hidden;
+ transition: all 0.6s ease-in 0s;
+}
+
+.front {
+ z-index: 20;
+}
+
+.back {
+ z-index: 10;
+}
+
+.wrapper {
+ --icon-size: 60%;
+
+ display: flex;
+ place-content: center;
+ place-items: center;
+ width: 100%;
+ height: 100%;
+ position: relative;
+ transition: all 0.5s ease-in-out 0s;
+ transform-style: preserve-3d;
+
+ &--active {
+ transform: rotateY(180deg);
+
+ .front {
+ transform: scale(0.2);
+ }
+
+ .back {
+ transform: scale(1) rotateY(180deg);
+ }
+ }
+
+ &--inactive {
+ .front {
+ transform: scale(1);
+ }
+
+ .back {
+ transform: scale(0.2) rotateY(180deg);
+ }
+ }
+}
diff --git a/src/components/molecules/forms/flipping-label.stories.tsx b/src/components/molecules/forms/flipping-label.stories.tsx
new file mode 100644
index 0000000..b8d17ec
--- /dev/null
+++ b/src/components/molecules/forms/flipping-label.stories.tsx
@@ -0,0 +1,96 @@
+import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import FlippingLabel from './flipping-label';
+
+export default {
+ title: 'Organisms/Forms/FlippingLabel',
+ component: FlippingLabel,
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible name for the label.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ children: {
+ control: {
+ type: null,
+ },
+ description: 'An icon for the label front face.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ htmlFor: {
+ control: {
+ type: null,
+ },
+ description: 'Bind the label to a field by id.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isActive: {
+ control: {
+ type: 'boolean',
+ },
+ description:
+ 'Which side of the label should be displayed? True for the close icon.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof FlippingLabel>;
+
+const Template: ComponentStory<typeof FlippingLabel> = ({
+ isActive,
+ ...args
+}) => {
+ const [active, setActive] = useState<boolean>(isActive);
+
+ return (
+ <div onClick={() => setActive(!active)}>
+ <FlippingLabel isActive={active} {...args} />
+ </div>
+ );
+};
+
+export const Active = Template.bind({});
+Active.args = {
+ children: <MagnifyingGlass />,
+ isActive: true,
+};
+
+export const Inactive = Template.bind({});
+Inactive.args = {
+ children: <MagnifyingGlass />,
+ isActive: false,
+};
diff --git a/src/components/molecules/forms/flipping-label.test.tsx b/src/components/molecules/forms/flipping-label.test.tsx
new file mode 100644
index 0000000..9a7aa22
--- /dev/null
+++ b/src/components/molecules/forms/flipping-label.test.tsx
@@ -0,0 +1,14 @@
+import { render, screen } from '@test-utils';
+import FlippingLabel from './flipping-label';
+
+describe('FlippingLabel', () => {
+ it('renders a label', () => {
+ const ariaLabel = 'vero quo inventore';
+ render(
+ <FlippingLabel aria-label={ariaLabel} isActive={false}>
+ <>Test</>
+ </FlippingLabel>
+ );
+ expect(screen.getByLabelText(ariaLabel)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/forms/flipping-label.tsx b/src/components/molecules/forms/flipping-label.tsx
new file mode 100644
index 0000000..c874369
--- /dev/null
+++ b/src/components/molecules/forms/flipping-label.tsx
@@ -0,0 +1,40 @@
+import Label, { LabelProps } from '@components/atoms/forms/label';
+import Close from '@components/atoms/icons/close';
+import { FC } from 'react';
+import styles from './flipping-label.module.scss';
+
+export type FlippingLabelProps = Pick<
+ LabelProps,
+ 'aria-label' | 'className' | 'htmlFor'
+> & {
+ /**
+ * The front icon.
+ */
+ children: JSX.Element;
+ /**
+ * Which side of the label should be displayed? True for the close icon.
+ */
+ isActive: boolean;
+};
+
+const FlippingLabel: FC<FlippingLabelProps> = ({
+ children,
+ className = '',
+ isActive,
+ ...props
+}) => {
+ const wrapperModifier = isActive ? 'wrapper--active' : 'wrapper--inactive';
+
+ return (
+ <Label className={`${styles.label} ${className}`} {...props}>
+ <span className={`${styles.wrapper} ${styles[wrapperModifier]}`}>
+ <span className={styles.front}>{children}</span>
+ <span className={styles.back}>
+ <Close />
+ </span>
+ </span>
+ </Label>
+ );
+};
+
+export default FlippingLabel;
diff --git a/src/components/molecules/forms/labelled-field.module.scss b/src/components/molecules/forms/labelled-field.module.scss
new file mode 100644
index 0000000..64ef3d0
--- /dev/null
+++ b/src/components/molecules/forms/labelled-field.module.scss
@@ -0,0 +1,9 @@
+.label {
+ &--left {
+ margin-right: var(--spacing-2xs);
+ }
+
+ &--top {
+ display: block;
+ }
+}
diff --git a/src/components/molecules/forms/labelled-field.stories.tsx b/src/components/molecules/forms/labelled-field.stories.tsx
new file mode 100644
index 0000000..795e785
--- /dev/null
+++ b/src/components/molecules/forms/labelled-field.stories.tsx
@@ -0,0 +1,293 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import LabelledField from './labelled-field';
+
+/**
+ * LabelledField - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Field',
+ component: LabelledField,
+ args: {
+ disabled: false,
+ hideLabel: false,
+ labelPosition: 'top',
+ required: false,
+ },
+ argTypes: {
+ 'aria-labelledby': {
+ control: {
+ type: 'text',
+ },
+ description: 'One or more ids that refers to the field name.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Field state: either enabled or disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ hideLabel: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Visually hide the field label.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field label.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ labelPosition: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label position.',
+ options: ['left', 'top'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'top' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ max: {
+ control: {
+ type: 'number',
+ },
+ description: 'Maximum value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ min: {
+ control: {
+ type: 'number',
+ },
+ description: 'Minimum value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ placeholder: {
+ control: {
+ type: 'text',
+ },
+ description: 'A placeholder value.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ required: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'Callback function to set field value.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ step: {
+ control: {
+ type: 'number',
+ },
+ description: 'Field incremental values that are valid.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ type: {
+ control: {
+ type: 'select',
+ },
+ description: 'Field type: input type or textarea.',
+ options: [
+ 'datetime-local',
+ 'email',
+ 'number',
+ 'search',
+ 'tel',
+ 'text',
+ 'textarea',
+ 'time',
+ 'url',
+ ],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'Field value.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof LabelledField>;
+
+const Template: ComponentStory<typeof LabelledField> = ({
+ value: _value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [value, setValue] = useState<string>('');
+
+ return <LabelledField value={value} setValue={setValue} {...args} />;
+};
+
+/**
+ * Labelled Field Stories - Left
+ */
+export const Left = Template.bind({});
+Left.args = {
+ id: 'labelled-field-storybook',
+ label: 'Labelled field',
+ labelPosition: 'left',
+ name: 'labelled-field-storybook',
+};
+
+/**
+ * Labelled Field Stories - Top
+ */
+export const Top = Template.bind({});
+Top.args = {
+ id: 'labelled-field-storybook',
+ label: 'Labelled field',
+ labelPosition: 'top',
+ name: 'labelled-field-storybook',
+};
+
+/**
+ * Labelled Field Stories - Required
+ */
+export const Required = Template.bind({});
+Required.args = {
+ id: 'labelled-field-storybook',
+ label: 'Labelled field',
+ name: 'labelled-field-storybook',
+ required: true,
+};
+
+/**
+ * Labelled Field Stories - Hidden label
+ */
+export const HiddenLabel = Template.bind({});
+HiddenLabel.args = {
+ hideLabel: true,
+ id: 'labelled-field-storybook',
+ label: 'Labelled field',
+ name: 'labelled-field-storybook',
+};
+
+/**
+ * Labelled Field Stories - Disabled
+ */
+export const Disabled = Template.bind({});
+Disabled.args = {
+ disabled: true,
+ id: 'labelled-field-storybook',
+ label: 'Labelled field',
+ name: 'labelled-field-storybook',
+};
diff --git a/src/components/molecules/forms/labelled-field.test.tsx b/src/components/molecules/forms/labelled-field.test.tsx
new file mode 100644
index 0000000..6fabe19
--- /dev/null
+++ b/src/components/molecules/forms/labelled-field.test.tsx
@@ -0,0 +1,19 @@
+import { render, screen } from '@test-utils';
+import LabelledField from './labelled-field';
+
+describe('LabelledField', () => {
+ it('renders a labelled field', () => {
+ render(
+ <LabelledField
+ type="text"
+ id="jest-text-field"
+ name="jest-text-field"
+ label="Jest text field"
+ value="test"
+ setValue={() => null}
+ />
+ );
+ expect(screen.getByLabelText('Jest text field')).toBeInTheDocument();
+ expect(screen.getByRole('textbox')).toHaveValue('test');
+ });
+});
diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx
new file mode 100644
index 0000000..6a00a3e
--- /dev/null
+++ b/src/components/molecules/forms/labelled-field.tsx
@@ -0,0 +1,50 @@
+import Field, { type FieldProps } from '@components/atoms/forms/field';
+import Label from '@components/atoms/forms/label';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
+import styles from './labelled-field.module.scss';
+
+export type LabelledFieldProps = FieldProps & {
+ /**
+ * Visually hide the field label. Default: false.
+ */
+ hideLabel?: boolean;
+ /**
+ * The field label.
+ */
+ label: string;
+ /**
+ * The label position. Default: top.
+ */
+ labelPosition?: 'left' | 'top';
+};
+
+/**
+ * LabelledField component
+ *
+ * Render a field tied to a label.
+ */
+const LabelledField: ForwardRefRenderFunction<
+ HTMLInputElement,
+ LabelledFieldProps
+> = (
+ { hideLabel = false, id, label, labelPosition = 'top', required, ...props },
+ ref
+) => {
+ const positionModifier = `label--${labelPosition}`;
+ const visibilityClass = hideLabel ? 'screen-reader-text' : '';
+
+ return (
+ <>
+ <Label
+ htmlFor={id}
+ required={required}
+ className={`${visibilityClass} ${styles[positionModifier]}`}
+ >
+ {label}
+ </Label>
+ <Field id={id} ref={ref} required={required} {...props} />
+ </>
+ );
+};
+
+export default forwardRef(LabelledField);
diff --git a/src/components/molecules/forms/labelled-select.module.scss b/src/components/molecules/forms/labelled-select.module.scss
new file mode 100644
index 0000000..64ef3d0
--- /dev/null
+++ b/src/components/molecules/forms/labelled-select.module.scss
@@ -0,0 +1,9 @@
+.label {
+ &--left {
+ margin-right: var(--spacing-2xs);
+ }
+
+ &--top {
+ display: block;
+ }
+}
diff --git a/src/components/molecules/forms/labelled-select.stories.tsx b/src/components/molecules/forms/labelled-select.stories.tsx
new file mode 100644
index 0000000..d02732c
--- /dev/null
+++ b/src/components/molecules/forms/labelled-select.stories.tsx
@@ -0,0 +1,236 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import LabelledSelect from './labelled-select';
+
+const selectOptions = [
+ { id: 'option1', name: 'Option 1', value: 'option1' },
+ { id: 'option2', name: 'Option 2', value: 'option2' },
+ { id: 'option3', name: 'Option 3', value: 'option3' },
+];
+
+/**
+ * LabelledSelect - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Select',
+ component: LabelledSelect,
+ args: {
+ disabled: false,
+ labelPosition: 'top',
+ required: false,
+ },
+ argTypes: {
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Field state: either enabled or disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field label.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ labelPosition: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label position.',
+ options: ['left', 'top'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'top' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ labelSize: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label size.',
+ options: ['medium', 'small'],
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ options: {
+ control: {
+ type: null,
+ },
+ description: 'Select options.',
+ type: {
+ name: 'array',
+ required: true,
+ value: {
+ name: 'string',
+ },
+ },
+ },
+ required: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ selectClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the select field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'Callback function to set field value.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'Field value.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof LabelledSelect>;
+
+const Template: ComponentStory<typeof LabelledSelect> = ({
+ value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [selected, setSelected] = useState<string>(value);
+
+ return <LabelledSelect value={selected} setValue={setSelected} {...args} />;
+};
+
+/**
+ * Labelled Select Stories - Left
+ */
+export const Left = Template.bind({});
+Left.args = {
+ id: 'labelled-select-storybook',
+ label: 'Labelled select',
+ labelPosition: 'left',
+ name: 'labelled-select-storybook',
+ options: selectOptions,
+ value: 'option1',
+};
+
+/**
+ * Labelled Select Stories - Top
+ */
+export const Top = Template.bind({});
+Top.args = {
+ id: 'labelled-select-storybook',
+ label: 'Labelled select',
+ labelPosition: 'top',
+ name: 'labelled-select-storybook',
+ options: selectOptions,
+ value: 'option1',
+};
+
+/**
+ * Labelled Select Stories - Disabled
+ */
+export const Disabled = Template.bind({});
+Disabled.args = {
+ disabled: true,
+ id: 'labelled-select-storybook',
+ label: 'Labelled select',
+ name: 'labelled-select-storybook',
+ options: selectOptions,
+ value: 'option1',
+};
+
+/**
+ * Labelled Select Stories - Required
+ */
+export const Required = Template.bind({});
+Required.args = {
+ id: 'labelled-select-storybook',
+ label: 'Labelled select',
+ labelPosition: 'top',
+ name: 'labelled-select-storybook',
+ options: selectOptions,
+ required: true,
+ value: 'option1',
+};
diff --git a/src/components/molecules/forms/labelled-select.test.tsx b/src/components/molecules/forms/labelled-select.test.tsx
new file mode 100644
index 0000000..9a50d6e
--- /dev/null
+++ b/src/components/molecules/forms/labelled-select.test.tsx
@@ -0,0 +1,25 @@
+import { render, screen } from '@test-utils';
+import LabelledSelect from './labelled-select';
+
+const selectOptions = [
+ { id: 'option1', name: 'Option 1', value: 'option1' },
+ { id: 'option2', name: 'Option 2', value: 'option2' },
+ { id: 'option3', name: 'Option 3', value: 'option3' },
+];
+
+describe('LabelledSelect', () => {
+ it('renders a labelled select', () => {
+ render(
+ <LabelledSelect
+ id="jest-select-field"
+ name="jest-select-field"
+ label="Jest select field"
+ options={selectOptions}
+ value="option1"
+ setValue={() => null}
+ />
+ );
+ expect(screen.getByLabelText('Jest select field')).toBeInTheDocument();
+ expect(screen.getByRole('combobox')).toHaveValue('option1');
+ });
+});
diff --git a/src/components/molecules/forms/labelled-select.tsx b/src/components/molecules/forms/labelled-select.tsx
new file mode 100644
index 0000000..23057d0
--- /dev/null
+++ b/src/components/molecules/forms/labelled-select.tsx
@@ -0,0 +1,69 @@
+import Label, { type LabelProps } from '@components/atoms/forms/label';
+import Select, { type SelectProps } from '@components/atoms/forms/select';
+import { FC } from 'react';
+import styles from './labelled-select.module.scss';
+
+export type LabelledSelectProps = Omit<
+ SelectProps,
+ 'aria-labelledby' | 'className'
+> & {
+ /**
+ * The field label.
+ */
+ label: string;
+ /**
+ * Set additional classnames to the label.
+ */
+ labelClassName?: LabelProps['className'];
+ /**
+ * The label position. Default: top.
+ */
+ labelPosition?: 'left' | 'top';
+ /**
+ * The label size.
+ */
+ labelSize?: LabelProps['size'];
+ /**
+ * Set additional classnames to the select field.
+ */
+ selectClassName?: SelectProps['className'];
+};
+
+/**
+ * LabelledSelect component
+ *
+ * Render a select with a label.
+ */
+const LabelledSelect: FC<LabelledSelectProps> = ({
+ id,
+ label,
+ labelClassName = '',
+ labelPosition = 'top',
+ labelSize,
+ required,
+ selectClassName = '',
+ ...props
+}) => {
+ const positionModifier = `label--${labelPosition}`;
+
+ return (
+ <>
+ <Label
+ htmlFor={id}
+ required={required}
+ size={labelSize}
+ className={`${styles[positionModifier]} ${labelClassName}`}
+ >
+ {label}
+ </Label>
+ <Select
+ id={id}
+ required={required}
+ {...props}
+ className={selectClassName}
+ />
+ </>
+ );
+};
+
+export default LabelledSelect;
diff --git a/src/components/molecules/forms/motion-toggle.stories.tsx b/src/components/molecules/forms/motion-toggle.stories.tsx
new file mode 100644
index 0000000..60430d5
--- /dev/null
+++ b/src/components/molecules/forms/motion-toggle.stories.tsx
@@ -0,0 +1,57 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import MotionToggleComponent from './motion-toggle';
+
+/**
+ * MotionToggle - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Toggle',
+ component: MotionToggleComponent,
+ argTypes: {
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ storageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'The reduce motion value.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof MotionToggleComponent>;
+
+const Template: ComponentStory<typeof MotionToggleComponent> = (args) => (
+ <MotionToggleComponent {...args} />
+);
+
+/**
+ * Toggle Stories - Motion
+ */
+export const Motion = Template.bind({});
+Motion.args = {
+ value: false,
+};
diff --git a/src/components/molecules/forms/motion-toggle.test.tsx b/src/components/molecules/forms/motion-toggle.test.tsx
new file mode 100644
index 0000000..4fd6b31
--- /dev/null
+++ b/src/components/molecules/forms/motion-toggle.test.tsx
@@ -0,0 +1,13 @@
+import { render, screen } from '@test-utils';
+import MotionToggle from './motion-toggle';
+
+describe('MotionToggle', () => {
+ it('renders a checked toggle (deactivate animations choice)', () => {
+ render(<MotionToggle storageKey="reduced-motion" value={true} />);
+ expect(
+ screen.getByRole('checkbox', {
+ name: `Animations: On Off`,
+ })
+ ).toBeChecked();
+ });
+});
diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx
new file mode 100644
index 0000000..cbe38fe
--- /dev/null
+++ b/src/components/molecules/forms/motion-toggle.tsx
@@ -0,0 +1,75 @@
+import Toggle, {
+ type ToggleChoices,
+ type ToggleProps,
+} from '@components/molecules/forms/toggle';
+import useAttributes from '@utils/hooks/use-attributes';
+import useLocalStorage from '@utils/hooks/use-local-storage';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+
+export type MotionToggleProps = Pick<
+ ToggleProps,
+ 'labelClassName' | 'value'
+> & {
+ /**
+ * The local storage key to save preference.
+ */
+ storageKey: string;
+};
+
+/**
+ * MotionToggle component
+ *
+ * Render a Toggle component to set reduce motion.
+ */
+const MotionToggle: FC<MotionToggleProps> = ({
+ storageKey,
+ value,
+ ...props
+}) => {
+ const intl = useIntl();
+ const { value: isReduced, setValue: setIsReduced } = useLocalStorage<boolean>(
+ storageKey,
+ value
+ );
+ useAttributes({
+ element: document.documentElement || undefined,
+ attribute: 'reduced-motion',
+ value: `${isReduced}`,
+ });
+
+ const reduceMotionLabel = intl.formatMessage({
+ defaultMessage: 'Animations:',
+ description: 'MotionToggle: reduce motion label',
+ id: '/q5csZ',
+ });
+ const onLabel = intl.formatMessage({
+ defaultMessage: 'On',
+ description: 'MotionToggle: activate reduce motion label',
+ id: 'va65iw',
+ });
+ const offLabel = intl.formatMessage({
+ defaultMessage: 'Off',
+ description: 'MotionToggle: deactivate reduce motion label',
+ id: 'pWKyyR',
+ });
+ const reduceMotionChoices: ToggleChoices = {
+ left: onLabel,
+ right: offLabel,
+ };
+
+ return (
+ <Toggle
+ id="reduce-motion-settings"
+ name="reduce-motion-settings"
+ label={reduceMotionLabel}
+ labelSize="medium"
+ choices={reduceMotionChoices}
+ value={isReduced}
+ setValue={setIsReduced}
+ {...props}
+ />
+ );
+};
+
+export default MotionToggle;
diff --git a/src/components/molecules/forms/prism-theme-toggle.stories.tsx b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
new file mode 100644
index 0000000..ef4ed6e
--- /dev/null
+++ b/src/components/molecules/forms/prism-theme-toggle.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import PrismThemeToggle from './prism-theme-toggle';
+
+/**
+ * PrismThemeToggle - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Toggle',
+ component: PrismThemeToggle,
+ argTypes: {
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof PrismThemeToggle>;
+
+const Template: ComponentStory<typeof PrismThemeToggle> = (args) => (
+ <PrismThemeToggle {...args} />
+);
+
+/**
+ * Toggle Stories - Prism theme
+ */
+export const PrismTheme = Template.bind({});
diff --git a/src/components/molecules/forms/prism-theme-toggle.test.tsx b/src/components/molecules/forms/prism-theme-toggle.test.tsx
new file mode 100644
index 0000000..c9d7894
--- /dev/null
+++ b/src/components/molecules/forms/prism-theme-toggle.test.tsx
@@ -0,0 +1,13 @@
+import { render, screen } from '@test-utils';
+import PrismThemeToggle from './prism-theme-toggle';
+
+describe('PrismThemeToggle', () => {
+ it('renders a toggle component', () => {
+ render(<PrismThemeToggle />);
+ expect(
+ screen.getByRole('checkbox', {
+ name: `Code blocks: Light theme Dark theme`,
+ })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx
new file mode 100644
index 0000000..3320722
--- /dev/null
+++ b/src/components/molecules/forms/prism-theme-toggle.tsx
@@ -0,0 +1,73 @@
+import Moon from '@components/atoms/icons/moon';
+import Sun from '@components/atoms/icons/sun';
+import Toggle, {
+ type ToggleChoices,
+ type ToggleProps,
+} from '@components/molecules/forms/toggle';
+import { usePrismTheme } from '@utils/providers/prism-theme';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+
+export type PrismThemeToggleProps = Pick<ToggleProps, 'labelClassName'>;
+
+/**
+ * PrismThemeToggle component
+ *
+ * Render a Toggle component to set code blocks theme.
+ */
+const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ ...props }) => {
+ const intl = useIntl();
+ const { theme, setTheme, resolvedTheme } = usePrismTheme();
+
+ /**
+ * Check if the resolved or chosen theme is dark theme.
+ *
+ * @returns {boolean} True if it is dark theme.
+ */
+ const isDarkTheme = (): boolean => {
+ if (theme === 'system') return resolvedTheme === 'dark';
+ return theme === 'dark';
+ };
+
+ /**
+ * Update the theme.
+ */
+ const updateTheme = () => {
+ setTheme(isDarkTheme() ? 'light' : 'dark');
+ };
+
+ const themeLabel = intl.formatMessage({
+ defaultMessage: 'Code blocks:',
+ description: 'PrismThemeToggle: theme label',
+ id: 'ftXN+0',
+ });
+ const lightThemeLabel = intl.formatMessage({
+ defaultMessage: 'Light theme',
+ description: 'PrismThemeToggle: light theme label',
+ id: 'tsWh8x',
+ });
+ const darkThemeLabel = intl.formatMessage({
+ defaultMessage: 'Dark theme',
+ description: 'PrismThemeToggle: dark theme label',
+ id: 'og/zWL',
+ });
+ const themeChoices: ToggleChoices = {
+ left: <Sun title={lightThemeLabel} />,
+ right: <Moon title={darkThemeLabel} />,
+ };
+
+ return (
+ <Toggle
+ id="prism-theme-settings"
+ name="prism-theme-settings"
+ label={themeLabel}
+ labelSize="medium"
+ choices={themeChoices}
+ value={isDarkTheme()}
+ setValue={updateTheme}
+ {...props}
+ />
+ );
+};
+
+export default PrismThemeToggle;
diff --git a/src/components/molecules/forms/select-with-tooltip.module.scss b/src/components/molecules/forms/select-with-tooltip.module.scss
new file mode 100644
index 0000000..bfadece
--- /dev/null
+++ b/src/components/molecules/forms/select-with-tooltip.module.scss
@@ -0,0 +1,48 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ position: relative;
+}
+
+.select {
+ width: auto;
+
+ @include mix.pointer("fine") {
+ padding: fun.convert-px(3) var(--spacing-xs);
+ }
+}
+
+.btn {
+ margin-left: var(--spacing-xs);
+
+ &--activated {
+ background: var(--color-primary);
+
+ * {
+ color: var(--color-fg-inverted);
+ }
+ }
+}
+
+.tooltip {
+ position: absolute;
+ top: calc(100% + var(--spacing-xs));
+ transform-origin: top;
+ transition: all 0.75s ease-in-out 0s;
+
+ &--hidden {
+ opacity: 0;
+ visibility: hidden;
+ transform: scale(0);
+ }
+
+ &--visible {
+ opacity: 1;
+ visibility: visible;
+ transform: scale(1);
+ }
+}
diff --git a/src/components/molecules/forms/select-with-tooltip.stories.tsx b/src/components/molecules/forms/select-with-tooltip.stories.tsx
new file mode 100644
index 0000000..ddf5d4c
--- /dev/null
+++ b/src/components/molecules/forms/select-with-tooltip.stories.tsx
@@ -0,0 +1,210 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import SelectWithTooltip from './select-with-tooltip';
+
+/**
+ * SelectWithTooltip - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Select',
+ component: SelectWithTooltip,
+ argTypes: {
+ content: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ disabled: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Field state: either enabled or disabled.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'The select label.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ labelSize: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label size.',
+ options: ['medium', 'small'],
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ options: {
+ control: {
+ type: null,
+ },
+ description: 'Select options.',
+ type: {
+ name: 'array',
+ required: true,
+ value: {
+ name: 'string',
+ },
+ },
+ },
+ required: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the field is required.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ selectClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the select field.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'Callback function to set field value.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip title',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ tooltipClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ value: {
+ control: {
+ type: 'text',
+ },
+ description: 'Field value.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SelectWithTooltip>;
+
+const selectOptions = [
+ { id: 'option1', name: 'Option 1', value: 'option1' },
+ { id: 'option2', name: 'Option 2', value: 'option2' },
+ { id: 'option3', name: 'Option 3', value: 'option3' },
+];
+
+const Template: ComponentStory<typeof SelectWithTooltip> = ({
+ value: _value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [selected, setSelected] = useState<string>('option1');
+ return (
+ <SelectWithTooltip value={selected} setValue={setSelected} {...args} />
+ );
+};
+
+/**
+ * Select Stories - With tooltip
+ */
+export const WithTooltip = Template.bind({});
+WithTooltip.args = {
+ content: 'Illo voluptatibus quia minima placeat sit nostrum excepturi.',
+ title: 'Possimus quidem dolor',
+ id: 'storybook-select',
+ label: 'Officiis:',
+ name: 'storybook-select',
+ options: selectOptions,
+};
diff --git a/src/components/molecules/forms/select-with-tooltip.test.tsx b/src/components/molecules/forms/select-with-tooltip.test.tsx
new file mode 100644
index 0000000..7a423f5
--- /dev/null
+++ b/src/components/molecules/forms/select-with-tooltip.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen } from '@test-utils';
+import SelectWithTooltip from './select-with-tooltip';
+
+const selectOptions = [
+ { id: 'option1', name: 'Option 1', value: 'option1' },
+ { id: 'option2', name: 'Option 2', value: 'option2' },
+ { id: 'option3', name: 'Option 3', value: 'option3' },
+];
+const selectLabel = 'Jest select';
+const selectValue = selectOptions[0].value;
+const tooltipTitle = 'Jest tooltip';
+const tooltipContent = 'Nesciunt voluptatibus voluptatem omnis at quia libero.';
+
+describe('SelectWithTooltip', () => {
+ it('renders a select', () => {
+ render(
+ <SelectWithTooltip
+ id="jest-select"
+ name="jest-select"
+ label={selectLabel}
+ options={selectOptions}
+ value={selectValue}
+ setValue={() => null}
+ title={tooltipTitle}
+ content={tooltipContent}
+ />
+ );
+ expect(screen.getByRole('combobox', { name: selectLabel })).toHaveValue(
+ selectValue
+ );
+ });
+});
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
new file mode 100644
index 0000000..29e2563
--- /dev/null
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -0,0 +1,73 @@
+import useClickOutside from '@utils/hooks/use-click-outside';
+import { FC, useRef, useState } from 'react';
+import HelpButton from '../buttons/help-button';
+import Tooltip, { type TooltipProps } from '../modals/tooltip';
+import LabelledSelect, { type LabelledSelectProps } from './labelled-select';
+import styles from './select-with-tooltip.module.scss';
+
+export type SelectWithTooltipProps = Omit<
+ LabelledSelectProps,
+ 'labelPosition'
+> &
+ Pick<TooltipProps, 'title' | 'content'> & {
+ /**
+ * Set additional classnames to the tooltip wrapper.
+ */
+ tooltipClassName?: TooltipProps['className'];
+ };
+
+/**
+ * SelectWithTooltip component
+ *
+ * Render a select with a button to display a tooltip about options.
+ */
+const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
+ title,
+ content,
+ id,
+ tooltipClassName = '',
+ ...props
+}) => {
+ const [isTooltipOpened, setIsTooltipOpened] = useState<boolean>(false);
+ const buttonRef = useRef<HTMLButtonElement>(null);
+ const tooltipRef = useRef<HTMLDivElement>(null);
+ const buttonModifier = isTooltipOpened ? styles['btn--activated'] : '';
+ const tooltipModifier = isTooltipOpened
+ ? styles['tooltip--visible']
+ : styles['tooltip--hidden'];
+
+ const closeTooltip = (target: EventTarget) => {
+ if (buttonRef.current && !buttonRef.current.contains(target as Node))
+ setIsTooltipOpened(false);
+ };
+
+ useClickOutside(
+ tooltipRef,
+ (target) => isTooltipOpened && closeTooltip(target)
+ );
+
+ return (
+ <div className={styles.wrapper}>
+ <LabelledSelect
+ labelPosition="left"
+ id={id}
+ labelClassName={styles.label}
+ {...props}
+ />
+ <HelpButton
+ className={`${styles.btn} ${buttonModifier}`}
+ onClick={() => setIsTooltipOpened(!isTooltipOpened)}
+ ref={buttonRef}
+ />
+ <Tooltip
+ title={title}
+ content={content}
+ icon="?"
+ className={`${styles.tooltip} ${tooltipModifier} ${tooltipClassName}`}
+ ref={tooltipRef}
+ />
+ </div>
+ );
+};
+
+export default SelectWithTooltip;
diff --git a/src/components/molecules/forms/theme-toggle.stories.tsx b/src/components/molecules/forms/theme-toggle.stories.tsx
new file mode 100644
index 0000000..5ebf5a2
--- /dev/null
+++ b/src/components/molecules/forms/theme-toggle.stories.tsx
@@ -0,0 +1,34 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ThemeToggle from './theme-toggle';
+
+/**
+ * ThemeToggle - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Toggle',
+ component: ThemeToggle,
+ argTypes: {
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ThemeToggle>;
+
+const Template: ComponentStory<typeof ThemeToggle> = (args) => (
+ <ThemeToggle {...args} />
+);
+
+/**
+ * Toggle Stories - Theme
+ */
+export const Theme = Template.bind({});
diff --git a/src/components/molecules/forms/theme-toggle.test.tsx b/src/components/molecules/forms/theme-toggle.test.tsx
new file mode 100644
index 0000000..0600c5e
--- /dev/null
+++ b/src/components/molecules/forms/theme-toggle.test.tsx
@@ -0,0 +1,13 @@
+import { render, screen } from '@test-utils';
+import ThemeToggle from './theme-toggle';
+
+describe('ThemeToggle', () => {
+ it('renders a toggle component', () => {
+ render(<ThemeToggle />);
+ expect(
+ screen.getByRole('checkbox', {
+ name: `Theme: Light theme Dark theme`,
+ })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx
new file mode 100644
index 0000000..61ee4c6
--- /dev/null
+++ b/src/components/molecules/forms/theme-toggle.tsx
@@ -0,0 +1,64 @@
+import Moon from '@components/atoms/icons/moon';
+import Sun from '@components/atoms/icons/sun';
+import Toggle, {
+ type ToggleChoices,
+ type ToggleProps,
+} from '@components/molecules/forms/toggle';
+import { useTheme } from 'next-themes';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+
+export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName'>;
+
+/**
+ * ThemeToggle component
+ *
+ * Render a Toggle component to set theme.
+ */
+const ThemeToggle: FC<ThemeToggleProps> = ({ ...props }) => {
+ const intl = useIntl();
+ const { resolvedTheme, setTheme } = useTheme();
+ const isDarkTheme = resolvedTheme === 'dark';
+
+ /**
+ * Update the theme.
+ */
+ const updateTheme = () => {
+ setTheme(isDarkTheme ? 'light' : 'dark');
+ };
+
+ const themeLabel = intl.formatMessage({
+ defaultMessage: 'Theme:',
+ description: 'ThemeToggle: theme label',
+ id: 'suXOBu',
+ });
+ const lightThemeLabel = intl.formatMessage({
+ defaultMessage: 'Light theme',
+ description: 'ThemeToggle: light theme label',
+ id: 'Ygea7s',
+ });
+ const darkThemeLabel = intl.formatMessage({
+ defaultMessage: 'Dark theme',
+ description: 'ThemeToggle: dark theme label',
+ id: '2QwvtS',
+ });
+ const themeChoices: ToggleChoices = {
+ left: <Sun title={lightThemeLabel} />,
+ right: <Moon title={darkThemeLabel} />,
+ };
+
+ return (
+ <Toggle
+ id="theme-settings"
+ name="theme-settings"
+ label={themeLabel}
+ labelSize="medium"
+ choices={themeChoices}
+ value={isDarkTheme}
+ setValue={updateTheme}
+ {...props}
+ />
+ );
+};
+
+export default ThemeToggle;
diff --git a/src/components/FormElements/Toggle/Toggle.module.scss b/src/components/molecules/forms/toggle.module.scss
index 48c88f6..2e8a49f 100644
--- a/src/components/FormElements/Toggle/Toggle.module.scss
+++ b/src/components/molecules/forms/toggle.module.scss
@@ -1,16 +1,16 @@
@use "@styles/abstracts/functions" as fun;
.label {
- --icon-size: #{fun.convert-px(25)};
--toggle-width: #{fun.convert-px(45)};
--toggle-height: calc(var(--toggle-width) / 2);
display: inline-flex;
align-items: center;
+ width: 100%;
}
.title {
- margin-right: var(--spacing-xs);
+ margin-right: var(--spacing-2xs);
}
.toggle {
@@ -28,8 +28,8 @@
&::after {
content: "";
display: block;
- width: calc(var(--toggle-width) / 2);
- height: calc(var(--toggle-width) / 2);
+ width: calc((var(--toggle-width) / 2) - 1px);
+ height: calc((var(--toggle-width) / 2) - 1px);
background: var(--color-primary-light);
border: fun.convert-px(1) solid var(--color-primary);
border-radius: 50%;
diff --git a/src/components/molecules/forms/toggle.stories.tsx b/src/components/molecules/forms/toggle.stories.tsx
new file mode 100644
index 0000000..0351ab7
--- /dev/null
+++ b/src/components/molecules/forms/toggle.stories.tsx
@@ -0,0 +1,121 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import Toggle from './toggle';
+
+/**
+ * ThemeToggle - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Forms/Toggle',
+ component: Toggle,
+ argTypes: {
+ choices: {
+ description: 'The toggle choices.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ id: {
+ control: {
+ type: 'text',
+ },
+ description: 'The input id.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ label: {
+ control: {
+ type: 'text',
+ },
+ description: 'The toggle label.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ labelClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the label.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ labelSize: {
+ control: {
+ type: 'select',
+ },
+ description: 'The label size.',
+ options: ['medium', 'small'],
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ name: {
+ control: {
+ type: 'text',
+ },
+ description: 'The input name.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ setValue: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to update the toggle value.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ value: {
+ control: {
+ type: null,
+ },
+ description: 'The toggle value. True if checked.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Toggle>;
+
+const Template: ComponentStory<typeof Toggle> = ({
+ value: _value,
+ setValue: _setValue,
+ ...args
+}) => {
+ const [isChecked, setIsChecked] = useState<boolean>(false);
+ return <Toggle value={isChecked} setValue={setIsChecked} {...args} />;
+};
+
+/**
+ * Toggle Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ choices: {
+ left: 'On',
+ right: 'Off',
+ },
+ id: 'toggle-example',
+ label: 'Activate setting:',
+ name: 'toggle-example',
+};
diff --git a/src/components/molecules/forms/toggle.test.tsx b/src/components/molecules/forms/toggle.test.tsx
new file mode 100644
index 0000000..fb97adc
--- /dev/null
+++ b/src/components/molecules/forms/toggle.test.tsx
@@ -0,0 +1,29 @@
+import { render, screen } from '@test-utils';
+import Toggle from './toggle';
+
+const choices = {
+ left: 'On',
+ right: 'Off',
+};
+
+const label = 'Activate this setting:';
+
+describe('Toggle', () => {
+ it('renders a checked toggle', () => {
+ render(
+ <Toggle
+ id="toggle-example"
+ name="toggle-example"
+ choices={choices}
+ label={label}
+ value={true}
+ setValue={(__value) => null}
+ />
+ );
+ expect(
+ screen.getByRole('checkbox', {
+ name: `${label} ${choices.left} ${choices.right}`,
+ })
+ ).toBeChecked();
+ });
+});
diff --git a/src/components/molecules/forms/toggle.tsx b/src/components/molecules/forms/toggle.tsx
new file mode 100644
index 0000000..288062d
--- /dev/null
+++ b/src/components/molecules/forms/toggle.tsx
@@ -0,0 +1,78 @@
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
+import Label, { type LabelProps } from '@components/atoms/forms/label';
+import { FC, ReactNode } from 'react';
+import styles from './toggle.module.scss';
+
+export type ToggleChoices = {
+ /**
+ * The left part of the toggle field (unchecked).
+ */
+ left: ReactNode;
+ /**
+ * The right part of the toggle field (checked).
+ */
+ right: ReactNode;
+};
+
+export type ToggleProps = Pick<CheckboxProps, 'id' | 'name'> & {
+ /**
+ * The toggle choices.
+ */
+ choices: ToggleChoices;
+ /**
+ * The toggle label.
+ */
+ label: string;
+ /**
+ * Set additional classnames to the label.
+ */
+ labelClassName?: LabelProps['className'];
+ /**
+ * The label size.
+ */
+ labelSize?: LabelProps['size'];
+ /**
+ * The toggle value. True if checked.
+ */
+ value: boolean;
+ /**
+ * A callback function to update the toggle value.
+ */
+ setValue: (value: boolean) => void;
+};
+
+/**
+ * Toggle component
+ *
+ * Render a toggle with a label and two choices.
+ */
+const Toggle: FC<ToggleProps> = ({
+ choices,
+ id,
+ label,
+ labelClassName = '',
+ labelSize,
+ name,
+ setValue,
+ value,
+}) => {
+ return (
+ <>
+ <Checkbox
+ name={name}
+ id={id}
+ value={value}
+ setValue={() => setValue(!value)}
+ className={styles.checkbox}
+ />
+ <Label size={labelSize} htmlFor={id} className={styles.label}>
+ <span className={`${styles.title} ${labelClassName}`}>{label}</span>
+ {choices.left}
+ <span className={styles.toggle}></span>
+ {choices.right}
+ </Label>
+ </>
+ );
+};
+
+export default Toggle;
diff --git a/src/components/molecules/images/flipping-logo.module.scss b/src/components/molecules/images/flipping-logo.module.scss
new file mode 100644
index 0000000..89b9499
--- /dev/null
+++ b/src/components/molecules/images/flipping-logo.module.scss
@@ -0,0 +1,59 @@
+@use "@styles/abstracts/functions" as fun;
+
+.logo {
+ width: var(--logo-size, fun.convert-px(100));
+ height: var(--logo-size, fun.convert-px(100));
+ position: relative;
+ border-radius: 50%;
+ transform-style: preserve-3d;
+ transition: all 0.6s linear 0s;
+
+ &__front,
+ &__back {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ backface-visibility: hidden;
+ background: var(--color-bg);
+ border: fun.convert-px(2) solid var(--color-primary-dark);
+ border-radius: 50%;
+ transition: all 0.6s linear 0s;
+
+ svg,
+ img {
+ // !important is required to override next/image styles...
+ padding: fun.convert-px(2) !important;
+ border-radius: 50%;
+ }
+ }
+
+ &__front {
+ box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0
+ var(--color-shadow-light),
+ fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0
+ var(--color-shadow-light);
+ }
+
+ &__back {
+ transform: rotateY(180deg);
+ }
+
+ &:hover {
+ transform: rotateY(180deg);
+ }
+
+ &:hover & {
+ &__front {
+ box-shadow: none;
+ }
+
+ &__back {
+ box-shadow: fun.convert-px(1) fun.convert-px(2) fun.convert-px(1) 0
+ var(--color-shadow-light),
+ fun.convert-px(2) fun.convert-px(3) fun.convert-px(3) 0
+ var(--color-shadow-light);
+ }
+ }
+}
diff --git a/src/components/molecules/images/flipping-logo.stories.tsx b/src/components/molecules/images/flipping-logo.stories.tsx
new file mode 100644
index 0000000..9d09293
--- /dev/null
+++ b/src/components/molecules/images/flipping-logo.stories.tsx
@@ -0,0 +1,72 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import FlippingLogoComponent from './flipping-logo';
+
+/**
+ * FlippingLogo - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Images',
+ component: FlippingLogoComponent,
+ argTypes: {
+ altText: {
+ control: {
+ type: 'text',
+ },
+ description: 'Photo alternative text.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the logo wrapper.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ logoTitle: {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible name for the logo.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ photo: {
+ control: {
+ type: 'text',
+ },
+ description: 'Photo url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof FlippingLogoComponent>;
+
+const Template: ComponentStory<typeof FlippingLogoComponent> = (args) => (
+ <FlippingLogoComponent {...args} />
+);
+
+/**
+ * Images Stories - Flipping Logo
+ */
+export const FlippingLogo = Template.bind({});
+FlippingLogo.args = {
+ altText: 'Website picture',
+ logoTitle: 'Website logo',
+ photo: 'http://placeimg.com/640/480',
+};
diff --git a/src/components/molecules/images/flipping-logo.test.tsx b/src/components/molecules/images/flipping-logo.test.tsx
new file mode 100644
index 0000000..806fdbe
--- /dev/null
+++ b/src/components/molecules/images/flipping-logo.test.tsx
@@ -0,0 +1,25 @@
+import { render, screen } from '@test-utils';
+import FlippingLogo from './flipping-logo';
+
+describe('FlippingLogo', () => {
+ it('renders a photo', () => {
+ render(
+ <FlippingLogo
+ altText="Alternative text"
+ photo="http://placeimg.com/640/480"
+ />
+ );
+ expect(screen.getByAltText('Alternative text')).toBeInTheDocument();
+ });
+
+ it('renders a logo', () => {
+ render(
+ <FlippingLogo
+ altText="Alternative text"
+ logoTitle="A logo title"
+ photo="http://placeimg.com/640/480"
+ />
+ );
+ expect(screen.getByTitle('A logo title')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx
new file mode 100644
index 0000000..1099d53
--- /dev/null
+++ b/src/components/molecules/images/flipping-logo.tsx
@@ -0,0 +1,55 @@
+import Logo, { type LogoProps } from '@components/atoms/images/logo';
+import Image, { type ImageProps } from 'next/image';
+import { ForwardedRef, forwardRef, ForwardRefRenderFunction } from 'react';
+import styles from './flipping-logo.module.scss';
+
+export type FlippingLogoProps = {
+ /**
+ * Set additional classnames to the logo wrapper.
+ */
+ className?: string;
+ /**
+ * Photo alternative text.
+ */
+ altText: string;
+ /**
+ * Logo image title.
+ */
+ logoTitle?: LogoProps['title'];
+ /**
+ * Photo url.
+ */
+ photo: ImageProps['src'];
+};
+
+/**
+ * FlippingLogo component
+ *
+ * Render a logo and a photo with a flipping effect.
+ */
+const FlippingLogo: ForwardRefRenderFunction<
+ HTMLDivElement,
+ FlippingLogoProps
+> = (
+ { className = '', altText, logoTitle, photo, ...props },
+ ref: ForwardedRef<HTMLDivElement>
+) => {
+ return (
+ <div className={`${styles.logo} ${className}`} ref={ref}>
+ <div className={styles.logo__front}>
+ <Image
+ src={photo}
+ alt={altText}
+ layout="fill"
+ objectFit="cover"
+ {...props}
+ />
+ </div>
+ <div className={styles.logo__back}>
+ <Logo title={logoTitle} />
+ </div>
+ </div>
+ );
+};
+
+export default forwardRef(FlippingLogo);
diff --git a/src/components/molecules/images/responsive-image.module.scss b/src/components/molecules/images/responsive-image.module.scss
new file mode 100644
index 0000000..8a1d51f
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.module.scss
@@ -0,0 +1,79 @@
+@use "@styles/abstracts/functions" as fun;
+
+.caption {
+ margin: 0;
+ padding: fun.convert-px(4) var(--spacing-2xs);
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(1) solid var(--color-border-light);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+}
+
+.wrapper {
+ display: flex;
+ flex-flow: column;
+ width: fit-content;
+ margin: 0 auto;
+ position: relative;
+ text-align: center;
+
+ &--has-borders {
+ .caption {
+ margin-top: fun.convert-px(4);
+ }
+ }
+
+ &--has-borders#{&}--has-link {
+ .link {
+ padding: fun.convert-px(4);
+ }
+ }
+
+ &--has-borders#{&}--no-link {
+ padding: fun.convert-px(4);
+ border: fun.convert-px(1) solid var(--color-border);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+ var(--color-shadow);
+ }
+}
+
+.link {
+ display: flex;
+ flex-flow: column;
+ background: none;
+ border: fun.convert-px(1) solid var(--color-border);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+ var(--color-shadow);
+ text-decoration: none;
+
+ .caption {
+ color: var(--color-primary-darker);
+ }
+
+ &:hover,
+ &:focus {
+ box-shadow: 0 0 fun.convert-px(2) 0 var(--color-shadow-light),
+ fun.convert-px(2) fun.convert-px(2) fun.convert-px(4) fun.convert-px(1)
+ var(--color-shadow-light),
+ fun.convert-px(4) fun.convert-px(4) fun.convert-px(8) fun.convert-px(2)
+ var(--color-shadow-light);
+ transform: scale(var(--scale-up, 1.05));
+ }
+
+ &:focus {
+ .caption {
+ text-decoration: underline solid var(--color-primary-darker)
+ fun.convert-px(3);
+ }
+ }
+
+ &:active {
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ fun.convert-px(1) var(--color-shadow-light);
+ transform: scale(var(--scale-down, 0.95));
+
+ .caption {
+ text-decoration: none;
+ }
+ }
+}
diff --git a/src/components/molecules/images/responsive-image.stories.tsx b/src/components/molecules/images/responsive-image.stories.tsx
new file mode 100644
index 0000000..4917cde
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.stories.tsx
@@ -0,0 +1,212 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ResponsiveImage from './responsive-image';
+
+/**
+ * ResponsiveImage - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Images/ResponsiveImage',
+ component: ResponsiveImage,
+ args: {
+ withBorders: false,
+ },
+ argTypes: {
+ alt: {
+ control: {
+ type: 'text',
+ },
+ description: 'An alternative text.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ caption: {
+ control: {
+ type: 'text',
+ },
+ description: 'A figure caption.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the image wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ height: {
+ control: {
+ type: 'number',
+ },
+ description: 'The image height.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ src: {
+ control: {
+ type: 'text',
+ },
+ description: 'The image source.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ target: {
+ control: {
+ type: 'text',
+ },
+ description: 'A link target.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ width: {
+ control: {
+ type: 'number',
+ },
+ description: 'The image width.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ withBorders: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Add borders around the image.',
+ table: {
+ category: 'Styles',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ResponsiveImage>;
+
+const Template: ComponentStory<typeof ResponsiveImage> = (args) => (
+ <ResponsiveImage {...args} />
+);
+
+/**
+ * Responsive Image Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+};
+
+/**
+ * Responsive Image Stories - With borders
+ */
+export const WithBorders = Template.bind({});
+WithBorders.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+ withBorders: true,
+};
+
+/**
+ * Responsive Image Stories - With link
+ */
+export const WithLink = Template.bind({});
+WithLink.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+ target: '#',
+};
+
+/**
+ * Responsive Image Stories - With link and borders
+ */
+export const WithLinkAndBorders = Template.bind({});
+WithLinkAndBorders.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+ target: '#',
+ withBorders: true,
+};
+
+/**
+ * Responsive Image Stories - With caption
+ */
+export const WithCaption = Template.bind({});
+WithCaption.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+ caption: 'Omnis nulla labore',
+};
+
+/**
+ * Responsive Image Stories - With caption and borders
+ */
+export const WithCaptionAndBorders = Template.bind({});
+WithCaptionAndBorders.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+ caption: 'Omnis nulla labore',
+ withBorders: true,
+};
+
+/**
+ * Responsive Image Stories - With caption and link
+ */
+export const WithCaptionAndLink = Template.bind({});
+WithCaptionAndLink.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+ caption: 'Omnis nulla labore',
+ target: '#',
+};
+
+/**
+ * Responsive Image Stories - With caption, link and borders
+ */
+export const WithCaptionLinkAndBorders = Template.bind({});
+WithCaptionLinkAndBorders.args = {
+ alt: 'An example',
+ src: 'http://placeimg.com/640/480/transport',
+ width: 640,
+ height: 480,
+ caption: 'Omnis nulla labore',
+ target: '#',
+ withBorders: true,
+};
diff --git a/src/components/molecules/images/responsive-image.test.tsx b/src/components/molecules/images/responsive-image.test.tsx
new file mode 100644
index 0000000..5452d28
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.test.tsx
@@ -0,0 +1,18 @@
+import { render, screen } from '@test-utils';
+import ResponsiveImage from './responsive-image';
+
+describe('ResponsiveImage', () => {
+ it('renders a responsive image', () => {
+ render(
+ <ResponsiveImage
+ src="http://placeimg.com/640/480"
+ alt="An alternative text"
+ width={640}
+ height={480}
+ />
+ );
+ expect(
+ screen.getByRole('img', { name: 'An alternative text' })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
new file mode 100644
index 0000000..4541df8
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -0,0 +1,95 @@
+import Link, { type LinkProps } from '@components/atoms/links/link';
+import Image, { type ImageProps } from 'next/image';
+import { FC, ReactNode } from 'react';
+import styles from './responsive-image.module.scss';
+
+export type ResponsiveImageProps = Omit<
+ ImageProps,
+ 'alt' | 'width' | 'height'
+> & {
+ /**
+ * An alternative text.
+ */
+ alt: string;
+ /**
+ * A figure caption.
+ */
+ caption?: ReactNode;
+ /**
+ * Set additional classnames to the figure wrapper.
+ */
+ className?: string;
+ /**
+ * The image height.
+ */
+ height: number | string;
+ /**
+ * A link target.
+ */
+ target?: LinkProps['href'];
+ /**
+ * The image width.
+ */
+ width: number | string;
+ /**
+ * Wrap the image with borders.
+ */
+ withBorders?: boolean;
+};
+
+/**
+ * ResponsiveImage component
+ *
+ * Render a responsive image wrapped in a figure element.
+ */
+const ResponsiveImage: FC<ResponsiveImageProps> = ({
+ alt,
+ caption,
+ className = '',
+ layout,
+ objectFit,
+ target,
+ withBorders,
+ ...props
+}) => {
+ const bordersModifier = withBorders
+ ? 'wrapper--has-borders'
+ : 'wrapper--no-borders';
+ const linkModifier = target ? 'wrapper--has-link' : 'wrapper--no-link';
+
+ return (
+ <figure
+ className={`${styles.wrapper} ${styles[bordersModifier]} ${styles[linkModifier]} ${className}`}
+ >
+ {target ? (
+ <Link href={target} className={styles.link}>
+ <Image
+ alt={alt}
+ layout={layout || 'intrinsic'}
+ objectFit={objectFit || 'contain'}
+ className={styles.img}
+ {...props}
+ />
+ {caption && (
+ <figcaption className={styles.caption}>{caption}</figcaption>
+ )}
+ </Link>
+ ) : (
+ <>
+ <Image
+ alt={alt}
+ layout={layout || 'intrinsic'}
+ objectFit={objectFit || 'contain'}
+ className={styles.img}
+ {...props}
+ />
+ {caption && (
+ <figcaption className={styles.caption}>{caption}</figcaption>
+ )}
+ </>
+ )}
+ </figure>
+ );
+};
+
+export default ResponsiveImage;
diff --git a/src/components/molecules/layout/branding.module.scss b/src/components/molecules/layout/branding.module.scss
new file mode 100644
index 0000000..6121fa1
--- /dev/null
+++ b/src/components/molecules/layout/branding.module.scss
@@ -0,0 +1,105 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+@mixin typing-animation {
+ --typing-animation: none;
+
+ width: fit-content;
+ position: relative;
+ overflow: hidden;
+
+ &::after {
+ content: "|";
+ display: block;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ right: 0;
+ background: var(--color-bg);
+ color: var(--color-primary-darker);
+ font-weight: 400;
+ text-align: left;
+ visibility: hidden;
+ transform: translateX(100%);
+ transform-origin: right;
+ animation: var(--typing-animation);
+
+ :global {
+ animation: var(--typing-animation);
+ }
+ }
+}
+
+.wrapper {
+ --logo-size: #{clamp(fun.convert-px(90), 12vw, fun.convert-px(100))};
+
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ justify-items: center;
+ width: 100%;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ grid-template-columns:
+ var(--logo-size, fun.convert-px(100))
+ minmax(0, 1fr);
+ grid-template-rows: 1fr min-content;
+ align-items: center;
+ justify-items: left;
+ column-gap: var(--spacing-sm);
+ width: unset;
+ }
+ }
+
+ .logo {
+ grid-row: span 2;
+ margin-bottom: var(--spacing-sm);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .title {
+ font-size: clamp(var(--font-size-xl), 8vw, var(--font-size-2xl));
+ text-align: center;
+
+ @include typing-animation;
+ }
+
+ .baseline {
+ color: var(--color-fg-light);
+ font-size: var(--font-size-lg);
+ text-align: center;
+
+ @include typing-animation;
+ }
+
+ .link {
+ background: linear-gradient(
+ to top,
+ var(--color-primary-light) fun.convert-px(5),
+ transparent fun.convert-px(5)
+ )
+ left / 0 100% no-repeat;
+ text-decoration: none;
+ transition: all 0.6s ease-out 0s;
+
+ &:hover,
+ &:focus {
+ background-size: 100% 100%;
+ }
+
+ &:focus {
+ color: var(--color-primary-light);
+ }
+
+ &:active {
+ background-size: 0 100%;
+ color: var(--color-primary-dark);
+ }
+ }
+}
diff --git a/src/components/molecules/layout/branding.stories.tsx b/src/components/molecules/layout/branding.stories.tsx
new file mode 100644
index 0000000..94bb166
--- /dev/null
+++ b/src/components/molecules/layout/branding.stories.tsx
@@ -0,0 +1,97 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Branding from './branding';
+
+/**
+ * Branding - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout/Branding',
+ component: Branding,
+ args: {
+ isHome: false,
+ withLink: false,
+ },
+ argTypes: {
+ baseline: {
+ control: {
+ type: 'text',
+ },
+ description: 'The Branding baseline.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isHome: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Use H1 if the current page is homepage.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ photo: {
+ control: {
+ type: 'text',
+ },
+ description: 'The Branding photo.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The Branding title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ withLink: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Wraps the title with a link to homepage.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Branding>;
+
+const Template: ComponentStory<typeof Branding> = (args) => (
+ <Branding {...args} />
+);
+
+/**
+ * Branding Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ title: 'Website title',
+ photo: 'http://placeimg.com/640/480',
+};
+
+/**
+ * Branding Stories - With baseline
+ */
+export const WithBaseline = Template.bind({});
+WithBaseline.args = {
+ title: 'Website title',
+ baseline: 'Maiores corporis qui',
+ photo: 'http://placeimg.com/640/480',
+};
diff --git a/src/components/molecules/layout/branding.test.tsx b/src/components/molecules/layout/branding.test.tsx
new file mode 100644
index 0000000..4fe1e9a
--- /dev/null
+++ b/src/components/molecules/layout/branding.test.tsx
@@ -0,0 +1,61 @@
+import { render, screen } from '@test-utils';
+import Branding from './branding';
+
+describe('Branding', () => {
+ it('renders a photo', () => {
+ render(
+ <Branding
+ photo="http://placeimg.com/640/480/city"
+ title="Website title"
+ />
+ );
+ expect(
+ screen.getByRole('img', { name: 'Website title picture' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a logo', () => {
+ render(
+ <Branding photo="http://placeimg.com/640/480/city" title="Website name" />
+ );
+ expect(screen.getByTitle('Website name logo')).toBeInTheDocument();
+ });
+
+ it('renders a baseline', () => {
+ render(
+ <Branding
+ photo="http://placeimg.com/640/480"
+ title="Website title"
+ baseline="Website baseline"
+ />
+ );
+ expect(screen.getByText('Website baseline')).toBeInTheDocument();
+ });
+
+ it('renders a title wrapped with h1 element', () => {
+ render(
+ <Branding
+ photo="http://placeimg.com/640/480"
+ title="Website title"
+ isHome={true}
+ />
+ );
+ expect(
+ screen.getByRole('heading', { level: 1, name: 'Website title' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a title with h1 styles', () => {
+ render(
+ <Branding
+ photo="http://placeimg.com/640/480"
+ title="Website title"
+ isHome={false}
+ />
+ );
+ expect(
+ screen.queryByRole('heading', { level: 1, name: 'Website title' })
+ ).not.toBeInTheDocument();
+ expect(screen.getByText('Website title')).toHaveClass('heading--1');
+ });
+});
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
new file mode 100644
index 0000000..9a82a74
--- /dev/null
+++ b/src/components/molecules/layout/branding.tsx
@@ -0,0 +1,119 @@
+import Heading from '@components/atoms/headings/heading';
+import useStyles from '@utils/hooks/use-styles';
+import Link from 'next/link';
+import { FC, useRef } from 'react';
+import { useIntl } from 'react-intl';
+import FlippingLogo, { type FlippingLogoProps } from '../images/flipping-logo';
+import styles from './branding.module.scss';
+
+export type BrandingProps = Pick<FlippingLogoProps, 'photo'> & {
+ /**
+ * The Branding baseline.
+ */
+ baseline?: string;
+ /**
+ * Use H1 if the current page is homepage. Default: false.
+ */
+ isHome?: boolean;
+ /**
+ * The Branding title;
+ */
+ title: string;
+ /**
+ * Wraps the title with a link to homepage. Default: false.
+ */
+ withLink?: boolean;
+};
+
+/**
+ * Branding component
+ *
+ * Render the branding logo, title and optional baseline.
+ */
+const Branding: FC<BrandingProps> = ({
+ baseline,
+ isHome = false,
+ photo,
+ title,
+ withLink = false,
+ ...props
+}) => {
+ const baselineRef = useRef<HTMLParagraphElement>(null);
+ const logoRef = useRef<HTMLDivElement>(null);
+ const titleRef = useRef<HTMLHeadingElement | HTMLParagraphElement>(null);
+ const intl = useIntl();
+ const altText = intl.formatMessage(
+ {
+ defaultMessage: '{website} picture',
+ description: 'Branding: photo alternative text',
+ id: 'dDK5oc',
+ },
+ { website: title }
+ );
+ const logoTitle = intl.formatMessage(
+ {
+ defaultMessage: '{website} logo',
+ description: 'Branding: logo title',
+ id: 'x55qsD',
+ },
+ { website: title }
+ );
+
+ useStyles({
+ property: '--typing-animation',
+ styles: 'blink 0.7s ease-in-out 0s 2, typing 4.3s linear 0s 1',
+ target: titleRef,
+ });
+ useStyles({
+ property: '--typing-animation',
+ styles:
+ 'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1',
+ target: baselineRef,
+ });
+ useStyles({
+ property: 'animation',
+ styles: 'flip-logo 9s ease-in 0s 1',
+ target: logoRef,
+ });
+
+ return (
+ <div className={styles.wrapper}>
+ <FlippingLogo
+ className={styles.logo}
+ altText={altText}
+ logoTitle={logoTitle}
+ photo={photo}
+ ref={logoRef}
+ {...props}
+ />
+ <Heading
+ isFake={!isHome}
+ level={1}
+ withMargin={false}
+ className={styles.title}
+ ref={titleRef}
+ >
+ {withLink ? (
+ <Link href="/">
+ <a className={styles.link}>{title}</a>
+ </Link>
+ ) : (
+ title
+ )}
+ </Heading>
+ {baseline && (
+ <Heading
+ isFake={true}
+ level={4}
+ withMargin={false}
+ className={styles.baseline}
+ ref={baselineRef}
+ >
+ {baseline}
+ </Heading>
+ )}
+ </div>
+ );
+};
+
+export default Branding;
diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss
new file mode 100644
index 0000000..6065642
--- /dev/null
+++ b/src/components/molecules/layout/card.module.scss
@@ -0,0 +1,87 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ --scale-up: 1.05;
+ --scale-down: 0.95;
+
+ display: flex;
+ flex-flow: column wrap;
+ max-width: var(--card-width, 40ch);
+ padding: 0;
+ text-align: center;
+
+ .article {
+ flex: 1;
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: flex-start;
+ }
+
+ .cover {
+ align-self: flex-start;
+ place-content: center;
+ height: fun.convert-px(150);
+ margin: auto;
+ border-bottom: fun.convert-px(1) solid var(--color-border);
+ }
+
+ .title,
+ .tagline,
+ .footer {
+ padding: 0 var(--spacing-md);
+ }
+
+ .title {
+ flex: 1;
+ margin-top: var(--spacing-sm);
+ margin-bottom: var(--spacing-sm);
+ }
+
+ h2.title {
+ background: none;
+ text-shadow: none;
+ }
+
+ .tagline {
+ flex: 1;
+ margin-bottom: var(--spacing-md);
+ color: var(--color-fg);
+ font-weight: 400;
+ }
+
+ .list {
+ margin-bottom: var(--spacing-md);
+ }
+
+ .meta {
+ &__item {
+ flex-flow: row wrap;
+ place-content: center;
+ gap: var(--spacing-2xs);
+ margin: auto;
+ }
+
+ &__label {
+ flex: 0 0 100%;
+ }
+
+ &__value {
+ padding: fun.convert-px(2) var(--spacing-xs);
+ border: fun.convert-px(1) solid var(--color-primary-darker);
+ color: var(--color-fg);
+ font-weight: 400;
+
+ &::before {
+ display: none;
+ }
+ }
+ }
+
+ &:not(:disabled):focus {
+ text-decoration: none;
+
+ .title {
+ text-decoration: underline solid var(--color-primary) 0.3ex;
+ }
+ }
+}
diff --git a/src/components/molecules/layout/card.stories.tsx b/src/components/molecules/layout/card.stories.tsx
new file mode 100644
index 0000000..0ad42c0
--- /dev/null
+++ b/src/components/molecules/layout/card.stories.tsx
@@ -0,0 +1,176 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Card from './card';
+
+/**
+ * Card - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout/Card',
+ component: Card,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the card wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ cover: {
+ description: 'The card cover data (src, dimensions, alternative text).',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ coverFit: {
+ control: {
+ type: 'select',
+ },
+ description: 'The cover fit.',
+ options: ['contain', 'cover', 'fill', 'scale-down'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'cover' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ meta: {
+ description: 'The card metadata (a publication date for example).',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ tagline: {
+ control: {
+ type: 'text',
+ },
+ description: 'A few words about the card.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The card title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The title level.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ url: {
+ control: {
+ type: 'text',
+ },
+ description: 'The card target.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Card>;
+
+const Template: ComponentStory<typeof Card> = (args) => <Card {...args} />;
+
+const cover = {
+ alt: 'A picture',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+};
+
+const meta = {
+ thematics: ['Autem', 'Eos'],
+};
+
+/**
+ * Card Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ title: 'Veritatis dicta quod',
+ titleLevel: 2,
+ url: '#',
+};
+
+/**
+ * Card Stories - With cover
+ */
+export const WithCover = Template.bind({});
+WithCover.args = {
+ cover,
+ title: 'Veritatis dicta quod',
+ titleLevel: 2,
+ url: '#',
+};
+
+/**
+ * Card Stories - With meta
+ */
+export const WithMeta = Template.bind({});
+WithMeta.args = {
+ meta,
+ title: 'Veritatis dicta quod',
+ titleLevel: 2,
+ url: '#',
+};
+
+/**
+ * Card Stories - With tagline
+ */
+export const WithTagline = Template.bind({});
+WithTagline.args = {
+ tagline: 'Ullam accusantium ipsa',
+ title: 'Veritatis dicta quod',
+ titleLevel: 2,
+ url: '#',
+};
+
+/**
+ * Card Stories - With all data
+ */
+export const WithAll = Template.bind({});
+WithAll.args = {
+ cover,
+ meta,
+ tagline: 'Ullam accusantium ipsa',
+ title: 'Veritatis dicta quod',
+ titleLevel: 2,
+ url: '#',
+};
diff --git a/src/components/molecules/layout/card.test.tsx b/src/components/molecules/layout/card.test.tsx
new file mode 100644
index 0000000..07c01e9
--- /dev/null
+++ b/src/components/molecules/layout/card.test.tsx
@@ -0,0 +1,49 @@
+import { render, screen } from '@test-utils';
+import Card from './card';
+
+const cover = {
+ alt: 'A picture',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+};
+
+const meta = {
+ author: 'Possimus',
+ thematics: ['Autem', 'Eos'],
+};
+
+const tagline = 'Ut rerum incidunt';
+
+const title = 'Alias qui porro';
+
+const url = '/an-existing-url';
+
+describe('Card', () => {
+ it('renders a title wrapped in h2 element', () => {
+ render(<Card title={title} titleLevel={2} url={url} />);
+ expect(
+ screen.getByRole('heading', { level: 2, name: title })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a link to another page', () => {
+ render(<Card title={title} titleLevel={2} url={url} />);
+ expect(screen.getByRole('link')).toHaveAttribute('href', url);
+ });
+
+ it('renders a cover', () => {
+ render(<Card title={title} titleLevel={2} url={url} cover={cover} />);
+ expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+ });
+
+ it('renders a tagline', () => {
+ render(<Card title={title} titleLevel={2} url={url} tagline={tagline} />);
+ expect(screen.getByText(tagline)).toBeInTheDocument();
+ });
+
+ it('renders some meta', () => {
+ render(<Card title={title} titleLevel={2} url={url} meta={meta} />);
+ expect(screen.getByText(meta.author)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
new file mode 100644
index 0000000..7bbd040
--- /dev/null
+++ b/src/components/molecules/layout/card.tsx
@@ -0,0 +1,98 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import { type Image } from '@ts/types/app';
+import { FC } from 'react';
+import ResponsiveImage, {
+ type ResponsiveImageProps,
+} from '../images/responsive-image';
+import styles from './card.module.scss';
+import Meta, { type MetaData } from './meta';
+
+export type CardProps = {
+ /**
+ * Set additional classnames to the card wrapper.
+ */
+ className?: string;
+ /**
+ * The card cover.
+ */
+ cover?: Image;
+ /**
+ * The cover fit. Default: cover.
+ */
+ coverFit?: ResponsiveImageProps['objectFit'];
+ /**
+ * The card meta.
+ */
+ meta?: MetaData;
+ /**
+ * The card tagline.
+ */
+ tagline?: string;
+ /**
+ * The card title.
+ */
+ title: string;
+ /**
+ * The title level (hn).
+ */
+ titleLevel: HeadingLevel;
+ /**
+ * The card target.
+ */
+ url: string;
+};
+
+/**
+ * Card component
+ *
+ * Render a link with minimal information about its content.
+ */
+const Card: FC<CardProps> = ({
+ className = '',
+ cover,
+ coverFit = 'cover',
+ meta,
+ tagline,
+ title,
+ titleLevel,
+ url,
+}) => {
+ return (
+ <ButtonLink target={url} className={`${styles.wrapper} ${className}`}>
+ <article className={styles.article}>
+ <header className={styles.header}>
+ {cover && (
+ <ResponsiveImage
+ {...cover}
+ objectFit={coverFit}
+ className={styles.cover}
+ />
+ )}
+ <Heading
+ alignment="center"
+ level={titleLevel}
+ className={styles.title}
+ >
+ {title}
+ </Heading>
+ </header>
+ <div className={styles.tagline}>{tagline}</div>
+ {meta && (
+ <footer className={styles.footer}>
+ <Meta
+ data={meta}
+ layout="inline"
+ className={styles.list}
+ groupClassName={styles.meta__item}
+ labelClassName={styles.meta__label}
+ valueClassName={styles.meta__value}
+ />
+ </footer>
+ )}
+ </article>
+ </ButtonLink>
+ );
+};
+
+export default Card;
diff --git a/src/components/molecules/layout/code.module.scss b/src/components/molecules/layout/code.module.scss
new file mode 100644
index 0000000..1feeccc
--- /dev/null
+++ b/src/components/molecules/layout/code.module.scss
@@ -0,0 +1,305 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ :global {
+ .code-toolbar {
+ --toolbar-height: #{fun.convert-px(100)};
+
+ position: relative;
+ margin-top: calc(var(--toolbar-height) + var(--spacing-sm));
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ --toolbar-height: #{fun.convert-px(60)};
+ }
+ }
+
+ .toolbar {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: center;
+ width: 100%;
+ height: var(--toolbar-height);
+ position: absolute;
+ top: calc(var(--toolbar-height) * -1);
+ left: 0;
+ right: 0;
+ background: var(--color-bg-tertiary);
+ border: fun.convert-px(1) solid var(--color-border);
+ }
+
+ .toolbar-item {
+ display: flex;
+ align-items: center;
+ margin: 0 var(--spacing-2xs);
+ }
+
+ .toolbar-item:nth-child(1) {
+ flex: 0 0 100%;
+ justify-content: center;
+ margin: 0 auto 0 0;
+ padding: 0 var(--spacing-sm);
+ background: var(--color-bg-code);
+ border-bottom: fun.convert-px(1) solid var(--color-border);
+ color: var(--color-primary-darker);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ flex: 0 0 auto;
+ justify-content: left;
+ border-bottom: none;
+ border-right: fun.convert-px(1) solid var(--color-border);
+ }
+ }
+ }
+ }
+
+ .copy-to-clipboard-button,
+ .prism-color-scheme-button {
+ display: block;
+ padding: fun.convert-px(3) var(--spacing-xs);
+ background: var(--color-bg);
+ border: 0.4ex solid var(--color-primary);
+ border-radius: fun.convert-px(30);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
+ var(--color-shadow),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
+ var(--color-shadow);
+ color: var(--color-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ transition: all 0.35s ease-in-out 0s;
+
+ &:hover,
+ &:focus {
+ transform: translateX(#{fun.convert-px(-2)})
+ translateY(#{fun.convert-px(-2)});
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow-light),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2)
+ fun.convert-px(-2) var(--color-shadow-light),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5)
+ fun.convert-px(-4) var(--color-shadow-light),
+ fun.convert-px(4) fun.convert-px(7) fun.convert-px(8)
+ fun.convert-px(-3) var(--color-shadow-light);
+ }
+
+ &:focus {
+ text-decoration: underline var(--color-primary) fun.convert-px(3);
+ }
+
+ &:active {
+ text-decoration: none;
+ transform: translateY(#{fun.convert-px(2)});
+ box-shadow: 0 0 0 0 var(--color-shadow);
+ }
+ }
+
+ pre[class*="language-"] {
+ --gutter-size-with-spacing: calc(var(--gutter-size) + var(--spacing-xs));
+
+ position: relative;
+ overflow: auto;
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(1) solid var(--color-border-light);
+ color: var(--color-fg);
+ hyphens: none;
+ tab-size: 4;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ word-wrap: normal;
+
+ &.command-line {
+ --gutter-size: 19ch;
+ padding-left: var(--gutter-size-with-spacing);
+ }
+
+ &.line-numbers {
+ --gutter-size: 6ch;
+
+ counter-reset: lineNumber;
+ padding-left: var(--gutter-size-with-spacing);
+ }
+
+ code {
+ display: block;
+ padding: var(--spacing-xs) 0;
+ position: relative;
+ }
+
+ .line-numbers-rows,
+ .command-line-prompt {
+ display: block;
+ width: var(--gutter-size);
+ padding: var(--spacing-xs) 0;
+ position: absolute;
+ top: 0;
+ left: calc(var(--gutter-size-with-spacing) * -1);
+ background: var(--color-bg);
+ border-right: fun.convert-px(1) solid var(--color-border);
+ font-size: 100%;
+ letter-spacing: -1px;
+ text-align: right;
+ pointer-events: none;
+ user-select: none;
+
+ > span {
+ &::before {
+ display: block;
+ padding-right: var(--spacing-xs);
+ color: var(--color-fg-light);
+ }
+ }
+ }
+
+ .command-line-prompt {
+ > span {
+ &::before {
+ content: " ";
+ }
+
+ &[data-user]::before {
+ content: "[" attr(data-user) "@" attr(data-host) "] $";
+ }
+
+ &[data-user="root"]::before {
+ content: "[" attr(data-user) "@" attr(data-host) "] #";
+ }
+
+ &[data-prompt]::before {
+ content: attr(data-prompt);
+ }
+
+ &[data-continuation-prompt]::before {
+ content: attr(data-continuation-prompt);
+ }
+ }
+ }
+
+ .line-numbers-rows {
+ > span {
+ counter-increment: lineNumber;
+
+ &::before {
+ content: counter(lineNumber);
+ }
+ }
+ }
+
+ .token {
+ &.comment,
+ &.doc-comment {
+ color: var(--color-fg-light);
+ }
+
+ &.punctuation {
+ color: var(--color-fg);
+ }
+
+ &.attr-name,
+ &.hexcode,
+ &.inserted,
+ &.string {
+ color: var(--color-token-green);
+ }
+
+ &.class,
+ &.coord,
+ &.id,
+ &.function {
+ color: var(--color-token-purple);
+ }
+
+ &.builtin,
+ &.builtin.class-name,
+ &.property-access,
+ &.regex,
+ &.scope {
+ color: var(--color-token-magenta);
+ }
+
+ &.class-name,
+ &.constant,
+ &.global,
+ &.interpolation,
+ &.key,
+ &.package,
+ &.this,
+ &.title,
+ &.variable {
+ color: var(--color-token-blue);
+ }
+
+ &.combinator,
+ &.keyword,
+ &.operator,
+ &.pseudo-class,
+ &.pseudo-element,
+ &.rule,
+ &.selector,
+ &.unit {
+ color: var(--color-token-orange);
+ }
+
+ &.attr-value,
+ &.boolean,
+ &.number {
+ color: var(--color-token-yellow);
+ }
+
+ &.delimiter,
+ &.doctype,
+ &.parameter,
+ &.parent,
+ &.property,
+ &.shebang,
+ &.tag {
+ color: var(--color-token-cyan);
+ }
+
+ &.deleted {
+ color: var(--color-token-red);
+ }
+
+ &.punctuation.brace-hover,
+ &.punctuation.brace-selected {
+ background: var(--color-bg);
+ outline: solid fun.convert-px(1) var(--color-primary-light);
+ }
+ }
+
+ span.inline-color-wrapper {
+ background: url(fun.encode-svg(
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="gray" d="M0 0h2v2H0z"/><path fill="white" d="M0 0h1v1H0zM1 1h1v1H1z"/></svg>'
+ ));
+
+ // Prevent repeating pattern to be seen.
+ background-position: center;
+ background-size: 110%;
+
+ display: inline-block;
+ height: 1.1ch;
+ width: 1.1ch;
+ margin: 0 0.5ch 0 0;
+ border: fun.convert-px(1) solid var(--color-bg);
+ outline: fun.convert-px(1) solid var(--color-border-dark);
+ overflow: hidden;
+ }
+
+ span.inline-color {
+ display: block;
+
+ /* To prevent visual glitches again */
+ height: 120%;
+ width: 120%;
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/layout/code.stories.tsx b/src/components/molecules/layout/code.stories.tsx
new file mode 100644
index 0000000..ac0e98f
--- /dev/null
+++ b/src/components/molecules/layout/code.stories.tsx
@@ -0,0 +1,110 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CodeComponent from './code';
+
+/**
+ * Code - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout/Code',
+ component: CodeComponent,
+ args: {
+ filterOutput: false,
+ outputPattern: '#output#',
+ },
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The code sample.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ filterOutput: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Filter the command line output.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ language: {
+ control: {
+ type: 'text',
+ },
+ description: 'The code sample language.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ plugins: {
+ description: 'An array of Prism plugins to activate.',
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ outputPattern: {
+ control: {
+ type: 'text',
+ },
+ description: 'The command line output pattern.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: '#output#' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CodeComponent>;
+
+const Template: ComponentStory<typeof CodeComponent> = (args) => (
+ <CodeComponent {...args} />
+);
+
+const javascriptCodeSample = `
+const foo = () => {
+ return 'bar';
+}
+`;
+
+/**
+ * Code Stories - Code sample
+ */
+export const CodeSample = Template.bind({});
+CodeSample.args = {
+ children: javascriptCodeSample,
+ language: 'javascript',
+ plugins: ['line-numbers'],
+};
+
+const commandLineCode = `
+ls -lah
+#output#drwxr-x---+ 42 armand armand 4,0K 17 avril 11:15 .
+#output#drwxr-xr-x 4 root root 4,0K 30 mai 2021 ..
+#output#-rw-r--r-- 1 armand armand 2,0K 21 juil. 2021 .xinitrc
+`;
+
+/**
+ * Code Stories - Command Line
+ */
+export const CommandLine = Template.bind({});
+CommandLine.args = {
+ children: commandLineCode,
+ filterOutput: true,
+ language: 'bash',
+ plugins: ['command-line'],
+};
diff --git a/src/components/molecules/layout/code.test.tsx b/src/components/molecules/layout/code.test.tsx
new file mode 100644
index 0000000..ebcfae5
--- /dev/null
+++ b/src/components/molecules/layout/code.test.tsx
@@ -0,0 +1,16 @@
+import { render } from '@test-utils';
+import Code from './code';
+
+const code = `
+function foo() {
+ return 'bar';
+}
+`;
+
+const language = 'javascript';
+
+describe('Code', () => {
+ it('renders a code block', () => {
+ render(<Code language={language}>{code}</Code>);
+ });
+});
diff --git a/src/components/molecules/layout/code.tsx b/src/components/molecules/layout/code.tsx
new file mode 100644
index 0000000..30351b9
--- /dev/null
+++ b/src/components/molecules/layout/code.tsx
@@ -0,0 +1,64 @@
+import usePrism, {
+ type OptionalPrismPlugin,
+ type PrismLanguage,
+} from '@utils/hooks/use-prism';
+import { FC, useRef } from 'react';
+import styles from './code.module.scss';
+
+export type CodeProps = {
+ /**
+ * The code to highlight.
+ */
+ children: string;
+ /**
+ * Filter command line output. Default: false.
+ */
+ filterOutput?: boolean;
+ /**
+ * The code language.
+ */
+ language: PrismLanguage;
+ /**
+ * The optional Prism plugins.
+ */
+ plugins?: OptionalPrismPlugin[];
+ /**
+ * Filter command line output using the given string. Default: #output#
+ */
+ outputPattern?: string;
+};
+
+/**
+ * Code component
+ *
+ * Render a code block with syntax highlighting.
+ */
+const Code: FC<CodeProps> = ({
+ children,
+ filterOutput = false,
+ language,
+ plugins = [],
+ outputPattern = '#output#',
+}) => {
+ const wrapperRef = useRef<HTMLDivElement>(null);
+ const { attributes, className } = usePrism({ language, plugins });
+
+ const outputAttribute = filterOutput
+ ? { 'data-filter-output': outputPattern }
+ : {};
+
+ return (
+ <div className={styles.wrapper} ref={wrapperRef}>
+ <pre
+ className={className}
+ tabIndex={0}
+ {...attributes}
+ {...outputAttribute}
+ >
+ <code className={`language-${language}`}>{children}</code>
+ </pre>
+ </div>
+ );
+};
+
+export default Code;
diff --git a/src/components/molecules/layout/columns.module.scss b/src/components/molecules/layout/columns.module.scss
new file mode 100644
index 0000000..b449c45
--- /dev/null
+++ b/src/components/molecules/layout/columns.module.scss
@@ -0,0 +1,30 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: grid;
+ gap: var(--spacing-md);
+
+ &--responsive#{&} {
+ @for $i from 2 through 4 {
+ &--#{$i}-columns {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ @include mix.dimensions("md") {
+ grid-template-columns: repeat($i, minmax(0, 1fr));
+ }
+ }
+ }
+ }
+ }
+
+ &--no-responsive#{&} {
+ @for $i from 2 through 4 {
+ &--#{$i}-columns {
+ grid-template-columns: repeat($i, minmax(0, 1fr));
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/layout/columns.stories.tsx b/src/components/molecules/layout/columns.stories.tsx
new file mode 100644
index 0000000..2022fa4
--- /dev/null
+++ b/src/components/molecules/layout/columns.stories.tsx
@@ -0,0 +1,108 @@
+import Column from '@components/atoms/layout/column';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Columns from './columns';
+
+export default {
+ title: 'Molecules/Layout/Columns',
+ args: {
+ responsive: true,
+ },
+ component: Columns,
+ argTypes: {
+ children: {
+ description: 'The columns.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the columns wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ count: {
+ control: {
+ type: 'number',
+ min: 2,
+ max: 4,
+ },
+ description: 'The number of columns.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ responsive: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Should the columns be stacked on small devices?',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: true },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Columns>;
+
+const Template: ComponentStory<typeof Columns> = (args) => (
+ <Columns {...args} />
+);
+
+const column1 =
+ 'Non praesentium voluptas quisquam ex est. Distinctio accusamus facilis libero in aut. Et veritatis quo impedit fugit amet sit accusantium. Ut est rerum asperiores sint libero eveniet. Molestias placeat recusandae suscipit eligendi sunt hic.';
+
+const column2 =
+ 'Occaecati consectetur ad similique itaque rem doloremque commodi voluptate porro. Nam quo voluptas commodi qui rerum qui. Explicabo quis adipisci rerum. Culpa alias laboriosam temporibus iusto harum at placeat.';
+
+const column3 =
+ 'Libero aut ab neque voluptatem commodi. Quam quia voluptatem iusto dolorum. Enim ipsa totam corrupti qui cum quidem ea. Eos sed aliquam porro consequatur officia sed.';
+
+const column4 =
+ 'Ratione placeat ea ea. Explicabo rem eaque voluptatibus. Nihil nulla culpa et dolor numquam omnis est. Quis quas excepturi est dignissimos ducimus et ad quis quis. Eos enim et nam delectus.';
+
+export const TwoColumns = Template.bind({});
+TwoColumns.args = {
+ children: [
+ <Column key="column-1">{column1}</Column>,
+ <Column key="column-2">{column2}</Column>,
+ <Column key="column-3">{column3}</Column>,
+ <Column key="column-4">{column4}</Column>,
+ ],
+ count: 2,
+};
+
+export const ThreeColumns = Template.bind({});
+ThreeColumns.args = {
+ children: [
+ <Column key="column-1">{column1}</Column>,
+ <Column key="column-2">{column2}</Column>,
+ <Column key="column-3">{column3}</Column>,
+ <Column key="column-4">{column4}</Column>,
+ ],
+ count: 3,
+};
+
+export const FourColumns = Template.bind({});
+FourColumns.args = {
+ children: [
+ <Column key="column-1">{column1}</Column>,
+ <Column key="column-2">{column2}</Column>,
+ <Column key="column-3">{column3}</Column>,
+ <Column key="column-4">{column4}</Column>,
+ ],
+ count: 4,
+};
diff --git a/src/components/molecules/layout/columns.test.tsx b/src/components/molecules/layout/columns.test.tsx
new file mode 100644
index 0000000..4b55bbb
--- /dev/null
+++ b/src/components/molecules/layout/columns.test.tsx
@@ -0,0 +1,48 @@
+import Column from '@components/atoms/layout/column';
+import { render, screen } from '@test-utils';
+import Columns from './columns';
+
+const column1 =
+ 'Non praesentium voluptas quisquam ex est. Distinctio accusamus facilis libero in aut. Et veritatis quo impedit fugit amet sit accusantium. Ut est rerum asperiores sint libero eveniet. Molestias placeat recusandae suscipit eligendi sunt hic.';
+
+const column2 =
+ 'Occaecati consectetur ad similique itaque rem doloremque commodi voluptate porro. Nam quo voluptas commodi qui rerum qui. Explicabo quis adipisci rerum. Culpa alias laboriosam temporibus iusto harum at placeat.';
+
+const column3 =
+ 'Libero aut ab neque voluptatem commodi. Quam quia voluptatem iusto dolorum. Enim ipsa totam corrupti qui cum quidem ea. Eos sed aliquam porro consequatur officia sed.';
+
+const column4 =
+ 'Ratione placeat ea ea. Explicabo rem eaque voluptatibus. Nihil nulla culpa et dolor numquam omnis est. Quis quas excepturi est dignissimos ducimus et ad quis quis. Eos enim et nam delectus.';
+
+describe('Columns', () => {
+ it('renders all the children', () => {
+ render(
+ <Columns count={2}>
+ <Column key="column-1">{column1}</Column>
+ <Column key="column-2">{column2}</Column>
+ <Column key="column-3">{column3}</Column>
+ <Column key="column-4">{column4}</Column>
+ </Columns>
+ );
+
+ expect(screen.getByText(column1)).toBeInTheDocument();
+ expect(screen.getByText(column2)).toBeInTheDocument();
+ expect(screen.getByText(column3)).toBeInTheDocument();
+ expect(screen.getByText(column4)).toBeInTheDocument();
+ });
+
+ it('renders the right number of columns', () => {
+ render(
+ <Columns count={3}>
+ <Column key="column-1">{column1}</Column>
+ <Column key="column-2">{column2}</Column>
+ <Column key="column-3">{column3}</Column>
+ <Column key="column-4">{column4}</Column>
+ </Columns>
+ );
+
+ const container = screen.getByText(column1).parentElement;
+
+ expect(container).toHaveClass('wrapper--3-columns');
+ });
+});
diff --git a/src/components/molecules/layout/columns.tsx b/src/components/molecules/layout/columns.tsx
new file mode 100644
index 0000000..c196457
--- /dev/null
+++ b/src/components/molecules/layout/columns.tsx
@@ -0,0 +1,49 @@
+import Column from '@components/atoms/layout/column';
+import { FC, ReactComponentElement } from 'react';
+import styles from './columns.module.scss';
+
+export type ColumnsProps = {
+ /**
+ * The columns.
+ */
+ children: ReactComponentElement<typeof Column>[];
+ /**
+ * Set additional classnames to the columns wrapper.
+ */
+ className?: string;
+ /**
+ * The number of columns.
+ */
+ count: 2 | 3 | 4;
+ /**
+ * Should the columns be stacked on small devices? Default: true.
+ */
+ responsive?: boolean;
+};
+
+/**
+ * Columns component.
+ *
+ * Render some Column components as columns.
+ */
+const Columns: FC<ColumnsProps> = ({
+ children,
+ className = '',
+ count,
+ responsive = true,
+}) => {
+ const countClass = `wrapper--${count}-columns`;
+ const responsiveClass = responsive
+ ? `wrapper--responsive`
+ : 'wrapper--no-responsive';
+
+ return (
+ <div
+ className={`${styles.wrapper} ${styles[countClass]} ${styles[responsiveClass]} ${className}`}
+ >
+ {children}
+ </div>
+ );
+};
+
+export default Columns;
diff --git a/src/components/molecules/layout/meta.module.scss b/src/components/molecules/layout/meta.module.scss
new file mode 100644
index 0000000..4194a6e
--- /dev/null
+++ b/src/components/molecules/layout/meta.module.scss
@@ -0,0 +1,5 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.value {
+ word-break: break-all;
+}
diff --git a/src/components/molecules/layout/meta.stories.tsx b/src/components/molecules/layout/meta.stories.tsx
new file mode 100644
index 0000000..c33680f
--- /dev/null
+++ b/src/components/molecules/layout/meta.stories.tsx
@@ -0,0 +1,69 @@
+import descriptionListItemStories from '@components/atoms/lists/description-list-item.stories';
+import descriptionListStories from '@components/atoms/lists/description-list.stories';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import MetaComponent, { MetaData } from './meta';
+
+/**
+ * Meta - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout',
+ component: MetaComponent,
+ args: {
+ itemsLayout: 'inline-values',
+ withSeparator: false,
+ },
+ argTypes: {
+ className: descriptionListStories.argTypes?.className,
+ data: {
+ description: 'The page metadata.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ groupClassName: descriptionListStories.argTypes?.groupClassName,
+ itemsLayout: {
+ ...descriptionListItemStories.argTypes?.layout,
+ table: {
+ ...descriptionListItemStories.argTypes?.layout?.table,
+ defaultValue: { summary: 'inline-values' },
+ },
+ },
+ labelClassName: descriptionListStories.argTypes?.labelClassName,
+ layout: descriptionListStories.argTypes?.layout,
+ valueClassName: descriptionListStories.argTypes?.valueClassName,
+ withSeparator: {
+ ...descriptionListStories.argTypes?.withSeparator,
+ table: {
+ ...descriptionListStories.argTypes?.withSeparator?.table,
+ defaultValue: { summary: true },
+ },
+ },
+ },
+} as ComponentMeta<typeof MetaComponent>;
+
+const Template: ComponentStory<typeof MetaComponent> = (args) => (
+ <MetaComponent {...args} />
+);
+
+const data: MetaData = {
+ publication: { date: '2022-04-09', time: '01:04:00' },
+ thematics: [
+ <a key="category1" href="#">
+ Category 1
+ </a>,
+ <a key="category2" href="#">
+ Category 2
+ </a>,
+ ],
+};
+
+/**
+ * Layout Stories - Meta
+ */
+export const Meta = Template.bind({});
+Meta.args = {
+ data,
+};
diff --git a/src/components/molecules/layout/meta.test.tsx b/src/components/molecules/layout/meta.test.tsx
new file mode 100644
index 0000000..fe66d97
--- /dev/null
+++ b/src/components/molecules/layout/meta.test.tsx
@@ -0,0 +1,24 @@
+import { render, screen } from '@test-utils';
+import { getFormattedDate } from '@utils/helpers/dates';
+import Meta from './meta';
+
+const data = {
+ publication: { date: '2022-04-09' },
+ thematics: [
+ <a key="category1" href="#">
+ Category 1
+ </a>,
+ <a key="category2" href="#">
+ Category 2
+ </a>,
+ ],
+};
+
+describe('Meta', () => {
+ it('format a date string', () => {
+ render(<Meta data={data} />);
+ expect(
+ screen.getByText(getFormattedDate(data.publication.date))
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
new file mode 100644
index 0000000..74bd4ff
--- /dev/null
+++ b/src/components/molecules/layout/meta.tsx
@@ -0,0 +1,391 @@
+import Link from '@components/atoms/links/link';
+import DescriptionList, {
+ type DescriptionListProps,
+ type DescriptionListItem,
+} from '@components/atoms/lists/description-list';
+import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates';
+import { FC, ReactNode } from 'react';
+import { useIntl } from 'react-intl';
+
+export type CustomMeta = {
+ label: string;
+ value: ReactNode | ReactNode[];
+};
+
+export type MetaComments = {
+ /**
+ * A page title.
+ */
+ about: string;
+ /**
+ * The comments count.
+ */
+ count: number;
+ /**
+ * Wrap the comments count with a link to the given target.
+ */
+ target?: string;
+};
+
+export type MetaDate = {
+ /**
+ * A date string. Ex: `2022-04-30`.
+ */
+ date: string;
+ /**
+ * A time string. Ex: `10:25:59`.
+ */
+ time?: string;
+ /**
+ * Wrap the date with a link to the given target.
+ */
+ target?: string;
+};
+
+export type MetaData = {
+ /**
+ * The author name.
+ */
+ author?: string;
+ /**
+ * The comments count.
+ */
+ comments?: MetaComments;
+ /**
+ * The creation date.
+ */
+ creation?: MetaDate;
+ /**
+ * A custom label/value metadata.
+ */
+ custom?: CustomMeta;
+ /**
+ * The license name.
+ */
+ license?: string;
+ /**
+ * The popularity.
+ */
+ popularity?: string | JSX.Element;
+ /**
+ * The publication date.
+ */
+ publication?: MetaDate;
+ /**
+ * The estimated reading time.
+ */
+ readingTime?: string | JSX.Element;
+ /**
+ * An array of repositories.
+ */
+ repositories?: string[] | JSX.Element[];
+ /**
+ * An array of technologies.
+ */
+ technologies?: string[];
+ /**
+ * An array of thematics.
+ */
+ thematics?: string[] | JSX.Element[];
+ /**
+ * An array of thematics.
+ */
+ topics?: string[] | JSX.Element[];
+ /**
+ * A total number of posts.
+ */
+ total?: number;
+ /**
+ * The update date.
+ */
+ update?: MetaDate;
+ /**
+ * An url.
+ */
+ website?: string;
+};
+
+export type MetaKey = keyof MetaData;
+
+export type MetaProps = Omit<
+ DescriptionListProps,
+ 'items' | 'withSeparator'
+> & {
+ /**
+ * The meta data.
+ */
+ data: MetaData;
+ /**
+ * The items layout.
+ */
+ itemsLayout?: DescriptionListItem['layout'];
+ /**
+ * If true, use a slash to delimitate multiple values. Default: true.
+ */
+ withSeparator?: DescriptionListProps['withSeparator'];
+};
+
+/**
+ * Meta component
+ *
+ * Renders the given metadata.
+ */
+const Meta: FC<MetaProps> = ({
+ data,
+ itemsLayout = 'inline-values',
+ withSeparator = true,
+ ...props
+}) => {
+ const intl = useIntl();
+
+ /**
+ * Retrieve the item label based on its key.
+ *
+ * @param {keyof MetaData} key - The meta key.
+ * @returns {string} The item label.
+ */
+ const getLabel = (key: keyof MetaData): string => {
+ switch (key) {
+ case 'author':
+ return intl.formatMessage({
+ defaultMessage: 'Written by:',
+ description: 'Meta: author label',
+ id: 'OI0N37',
+ });
+ case 'comments':
+ return intl.formatMessage({
+ defaultMessage: 'Comments:',
+ description: 'Meta: comments label',
+ id: 'jTVIh8',
+ });
+ case 'creation':
+ return intl.formatMessage({
+ defaultMessage: 'Created on:',
+ description: 'Meta: creation date label',
+ id: 'b4fdYE',
+ });
+ case 'license':
+ return intl.formatMessage({
+ defaultMessage: 'License:',
+ description: 'Meta: license label',
+ id: 'AuGklx',
+ });
+ case 'popularity':
+ return intl.formatMessage({
+ defaultMessage: 'Popularity:',
+ description: 'Meta: popularity label',
+ id: 'pWTj2W',
+ });
+ case 'publication':
+ return intl.formatMessage({
+ defaultMessage: 'Published on:',
+ description: 'Meta: publication date label',
+ id: 'QGi5uD',
+ });
+ case 'readingTime':
+ return intl.formatMessage({
+ defaultMessage: 'Reading time:',
+ description: 'Meta: reading time label',
+ id: 'EbFvsM',
+ });
+ case 'repositories':
+ return intl.formatMessage({
+ defaultMessage: 'Repositories:',
+ description: 'Meta: repositories label',
+ id: 'DssFG1',
+ });
+ case 'technologies':
+ return intl.formatMessage({
+ defaultMessage: 'Technologies:',
+ description: 'Meta: technologies label',
+ id: 'ADQmDF',
+ });
+ case 'thematics':
+ return intl.formatMessage({
+ defaultMessage: 'Thematics:',
+ description: 'Meta: thematics label',
+ id: 'bz53Us',
+ });
+ case 'topics':
+ return intl.formatMessage({
+ defaultMessage: 'Topics:',
+ description: 'Meta: topics label',
+ id: 'gJNaBD',
+ });
+ case 'total':
+ return intl.formatMessage({
+ defaultMessage: 'Total:',
+ description: 'Meta: total label',
+ id: '92zgdp',
+ });
+ case 'update':
+ return intl.formatMessage({
+ defaultMessage: 'Updated on:',
+ description: 'Meta: update date label',
+ id: 'tLC7bh',
+ });
+ case 'website':
+ return intl.formatMessage({
+ defaultMessage: 'Official website:',
+ description: 'Meta: official website label',
+ id: 'GRyyfy',
+ });
+ default:
+ return '';
+ }
+ };
+
+ /**
+ * Retrieve a formatted date (and time).
+ *
+ * @param {MetaDate} dateTime - A date object.
+ * @returns {JSX.Element} The formatted date wrapped in a time element.
+ */
+ const getDate = (dateTime: MetaDate): JSX.Element => {
+ const { date, time, target } = dateTime;
+
+ if (!dateTime.time) {
+ const isoDate = new Date(`${date}`).toISOString();
+ return target ? (
+ <Link href={target}>
+ <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time>
+ </Link>
+ ) : (
+ <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time>
+ );
+ }
+
+ const isoDateTime = new Date(`${date}T${time}`).toISOString();
+ const dateString = intl.formatMessage(
+ {
+ defaultMessage: '{date} at {time}',
+ description: 'Meta: publication date and time',
+ id: 'fcHeyC',
+ },
+ {
+ date: getFormattedDate(dateTime.date),
+ time: getFormattedTime(`${dateTime.date}T${dateTime.time}`),
+ }
+ );
+
+ return target ? (
+ <Link href={target}>
+ <time dateTime={isoDateTime}>{dateString}</time>
+ </Link>
+ ) : (
+ <time dateTime={isoDateTime}>{dateString}</time>
+ );
+ };
+
+ /**
+ * Retrieve the formatted comments count.
+ *
+ * @param comments - The comments object.
+ * @returns {string | JSX.Element} - The comments count.
+ */
+ const getCommentsCount = (comments: MetaComments): string | JSX.Element => {
+ const { about, count, target } = comments;
+ const commentsCount = intl.formatMessage(
+ {
+ defaultMessage:
+ '{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>',
+ description: 'Meta: comments count',
+ id: '02rgLO',
+ },
+ {
+ a11y: (chunks: ReactNode) => (
+ <span className="screen-reader-text">{chunks}</span>
+ ),
+ commentsCount: count,
+ title: about,
+ }
+ );
+
+ return target ? (
+ <Link href={target}>{commentsCount as JSX.Element}</Link>
+ ) : (
+ (commentsCount as JSX.Element)
+ );
+ };
+
+ /**
+ * Retrieve the formatted item value.
+ *
+ * @param {keyof MetaData} key - The meta key.
+ * @param {ValueOf<MetaData>} value - The meta value.
+ * @returns {string|ReactNode|ReactNode[]} - The formatted value.
+ */
+ const getValue = <T extends MetaKey>(
+ key: T,
+ value: MetaData[T]
+ ): string | ReactNode | ReactNode[] => {
+ switch (key) {
+ case 'comments':
+ return getCommentsCount(value as MetaComments);
+ case 'creation':
+ case 'publication':
+ case 'update':
+ return getDate(value as MetaDate);
+ case 'total':
+ return intl.formatMessage(
+ {
+ defaultMessage:
+ '{postsCount, plural, =0 {No articles} one {# article} other {# articles}}',
+ description: 'BlogPage: posts count meta',
+ id: 'OF5cPz',
+ },
+ { postsCount: value as number }
+ );
+ case 'website':
+ const url = value as string;
+ return (
+ <Link href={url} external={true}>
+ {url}
+ </Link>
+ );
+ default:
+ return value as string | ReactNode | ReactNode[];
+ }
+ };
+
+ /**
+ * Transform the metadata to description list item format.
+ *
+ * @param {MetaData} items - The meta.
+ * @returns {DescriptionListItem[]} The formatted description list items.
+ */
+ const getItems = (items: MetaData): DescriptionListItem[] => {
+ const listItems: DescriptionListItem[] = Object.entries(items)
+ .map(([key, value]) => {
+ if (!key || !value) return;
+
+ const metaKey = key as MetaKey;
+
+ return {
+ id: metaKey,
+ label:
+ metaKey === 'custom'
+ ? (value as CustomMeta).label
+ : getLabel(metaKey),
+ layout: itemsLayout,
+ value:
+ metaKey === 'custom' && (value as CustomMeta)
+ ? (value as CustomMeta).value
+ : getValue(metaKey, value),
+ } as DescriptionListItem;
+ })
+ .filter((item): item is DescriptionListItem => !!item);
+
+ return listItems;
+ };
+
+ return (
+ <DescriptionList
+ items={getItems(data)}
+ withSeparator={withSeparator}
+ {...props}
+ />
+ );
+};
+
+export default Meta;
diff --git a/src/components/molecules/layout/page-footer.stories.tsx b/src/components/molecules/layout/page-footer.stories.tsx
new file mode 100644
index 0000000..31b7a49
--- /dev/null
+++ b/src/components/molecules/layout/page-footer.stories.tsx
@@ -0,0 +1,60 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { MetaData } from './meta';
+import PageFooterComponent from './page-footer';
+
+/**
+ * Page Footer - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout',
+ component: PageFooterComponent,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the footer element.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ meta: {
+ description: 'The page meta.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof PageFooterComponent>;
+
+const Template: ComponentStory<typeof PageFooterComponent> = (args) => (
+ <PageFooterComponent {...args} />
+);
+
+const meta: MetaData = {
+ custom: {
+ label: 'More posts about:',
+ value: [
+ <a key="topic-1" href="#">
+ Topic name
+ </a>,
+ ],
+ },
+};
+
+/**
+ * Page Footer Stories - With meta
+ */
+export const PageFooter = Template.bind({});
+PageFooter.args = {
+ meta,
+};
diff --git a/src/components/molecules/layout/page-footer.test.tsx b/src/components/molecules/layout/page-footer.test.tsx
new file mode 100644
index 0000000..2e95625
--- /dev/null
+++ b/src/components/molecules/layout/page-footer.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import PageFooter from './page-footer';
+
+describe('PageFooter', () => {
+ it('renders a footer element', () => {
+ render(<PageFooter />);
+ expect(screen.getByRole('contentinfo')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/layout/page-footer.tsx b/src/components/molecules/layout/page-footer.tsx
new file mode 100644
index 0000000..97e449f
--- /dev/null
+++ b/src/components/molecules/layout/page-footer.tsx
@@ -0,0 +1,28 @@
+import { FC } from 'react';
+import Meta, { MetaData } from './meta';
+
+export type PageFooterProps = {
+ /**
+ * Set additional classnames to the footer element.
+ */
+ className?: string;
+ /**
+ * The footer metadata.
+ */
+ meta?: MetaData;
+};
+
+/**
+ * PageFooter component
+ *
+ * Render a footer element to display page meta.
+ */
+const PageFooter: FC<PageFooterProps> = ({ meta, ...props }) => {
+ return (
+ <footer {...props}>
+ {meta && <Meta data={meta} withSeparator={false} />}
+ </footer>
+ );
+};
+
+export default PageFooter;
diff --git a/src/components/PostHeader/PostHeader.module.scss b/src/components/molecules/layout/page-header.module.scss
index f813060..232023a 100644
--- a/src/components/PostHeader/PostHeader.module.scss
+++ b/src/components/molecules/layout/page-header.module.scss
@@ -1,20 +1,8 @@
@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
.wrapper {
- composes: grid from "@styles/layout/_grid.scss";
- max-width: 100%;
- position: relative;
-
- @include mix.media("screen") {
- @include mix.dimensions("md") {
- margin-bottom: var(--spacing-md);
- }
-
- @include mix.dimensions("lg") {
- --grid-gap: var(--spacing-lg);
- }
- }
+ @extend %grid;
&::before,
&::after {
@@ -41,17 +29,16 @@
.body {
grid-column: 2;
- background: var(--color-bg);
+ display: flex;
+ flex-flow: column wrap;
+ row-gap: var(--spacing-sm);
}
.title {
- flex: 0 0 100%;
display: flex;
flex-flow: row wrap;
align-items: center;
- margin: 0;
position: relative;
- text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
&::before,
&::after {
@@ -66,17 +53,11 @@
}
}
-.cover {
- display: block;
- width: fun.convert-px(50);
- height: fun.convert-px(50);
- position: relative;
- margin-right: var(--spacing-sm);
+.meta {
+ font-size: var(--font-size-sm);
}
.intro {
- margin: var(--spacing-sm) 0 0;
-
> *:last-child {
margin-bottom: 0;
}
diff --git a/src/components/molecules/layout/page-header.stories.tsx b/src/components/molecules/layout/page-header.stories.tsx
new file mode 100644
index 0000000..d58f8b5
--- /dev/null
+++ b/src/components/molecules/layout/page-header.stories.tsx
@@ -0,0 +1,113 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import PageHeader from './page-header';
+
+/**
+ * Page Header - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout/PageHeader',
+ component: PageHeader,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the header element.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ intro: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page introduction.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ meta: {
+ description: 'The page metadata.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof PageHeader>;
+
+const Template: ComponentStory<typeof PageHeader> = (args) => (
+ <PageHeader {...args} />
+);
+
+const meta = {
+ publication: { date: '2022-04-09' },
+ thematics: [
+ <a key="category1" href="#">
+ Category 1
+ </a>,
+ <a key="category2" href="#">
+ Category 2
+ </a>,
+ ],
+};
+
+/**
+ * Page Header Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ title: 'Excepturi nesciunt illum',
+};
+
+/**
+ * Page Header Stories - With introduction
+ */
+export const WithIntro = Template.bind({});
+WithIntro.args = {
+ intro:
+ 'Minima dolor nihil. Velit atque odit totam enim. Quisquam reprehenderit ut et inventore et nihil libero exercitationem. Cumque similique magni placeat et. Et sed est cumque labore. Et quia similique.',
+ title: 'Excepturi nesciunt illum',
+};
+
+/**
+ * Page Header Stories - With meta
+ */
+export const WithMeta = Template.bind({});
+WithMeta.args = {
+ meta,
+ title: 'Excepturi nesciunt illum',
+};
+
+/**
+ * Page Header Stories - With introduction and meta
+ */
+export const WithIntroAndMeta = Template.bind({});
+WithIntroAndMeta.args = {
+ intro:
+ 'Minima dolor nihil. Velit atque odit totam enim. Quisquam reprehenderit ut et inventore et nihil libero exercitationem. Cumque similique magni placeat et. Et sed est cumque labore. Et quia similique.',
+ meta,
+ title: 'Excepturi nesciunt illum',
+};
diff --git a/src/components/molecules/layout/page-header.test.tsx b/src/components/molecules/layout/page-header.test.tsx
new file mode 100644
index 0000000..329b54c
--- /dev/null
+++ b/src/components/molecules/layout/page-header.test.tsx
@@ -0,0 +1,18 @@
+import { render, screen } from '@test-utils';
+import PageHeader from './page-header';
+
+const title = 'Non nemo amet';
+const intro =
+ 'Suscipit omnis minima doloribus commodi. Laudantium similique ut enim voluptatem soluta maxime autem et.';
+
+describe('PageHeader', () => {
+ it('renders a title', () => {
+ render(<PageHeader title={title} intro={intro} />);
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(title);
+ });
+
+ it('renders an introduction', () => {
+ render(<PageHeader title={title} intro={intro} />);
+ expect(screen.getByText(intro)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx
new file mode 100644
index 0000000..6759c7f
--- /dev/null
+++ b/src/components/molecules/layout/page-header.tsx
@@ -0,0 +1,67 @@
+import Heading from '@components/atoms/headings/heading';
+import { FC, ReactNode } from 'react';
+import Meta, { type MetaData } from './meta';
+import styles from './page-header.module.scss';
+
+export type PageHeaderProps = {
+ /**
+ * Set additional classnames to the header element.
+ */
+ className?: string;
+ /**
+ * The page introduction.
+ */
+ intro?: string | JSX.Element;
+ /**
+ * The page metadata.
+ */
+ meta?: MetaData;
+ /**
+ * The page title.
+ */
+ title: ReactNode;
+};
+
+/**
+ * PageHeader component
+ *
+ * Render a header element with page title, meta and intro.
+ */
+const PageHeader: FC<PageHeaderProps> = ({
+ className = '',
+ intro,
+ meta,
+ title,
+}) => {
+ const getIntro = () => {
+ return typeof intro === 'string' ? (
+ <div
+ className={styles.intro}
+ dangerouslySetInnerHTML={{ __html: intro }}
+ />
+ ) : (
+ <div className={styles.intro}>{intro}</div>
+ );
+ };
+
+ return (
+ <header className={`${styles.wrapper} ${className}`}>
+ <div className={styles.body}>
+ <Heading level={1} className={styles.title} withMargin={false}>
+ {title}
+ </Heading>
+ {meta && (
+ <Meta
+ data={meta}
+ className={styles.meta}
+ layout="column"
+ itemsLayout="inline"
+ />
+ )}
+ {intro && getIntro()}
+ </div>
+ </header>
+ );
+};
+
+export default PageHeader;
diff --git a/src/components/molecules/layout/widget.module.scss b/src/components/molecules/layout/widget.module.scss
new file mode 100644
index 0000000..27d7ffd
--- /dev/null
+++ b/src/components/molecules/layout/widget.module.scss
@@ -0,0 +1,65 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.widget {
+ display: flex;
+ flex-flow: column;
+
+ &__header {
+ z-index: 2;
+ background: var(--color-bg);
+ }
+
+ &__body {
+ position: relative;
+ }
+
+ &--has-borders & {
+ &__body {
+ border: fun.convert-px(2) solid var(--color-primary-dark);
+ }
+ }
+
+ &--collapsed & {
+ &__body {
+ max-height: 0;
+ margin: 0;
+ visibility: hidden;
+ opacity: 0;
+ overflow: hidden;
+ border: 0 solid transparent;
+ transition: all 0.1s linear 0.3s,
+ max-height 0.5s cubic-bezier(0, 1, 0, 1) 0s, margin 0.3s ease-in-out 0s;
+ }
+ }
+
+ &--expanded#{&}--has-scroll {
+ @include mix.media("screen") {
+ @include mix.dimensions("lg") {
+ max-height: 95vh;
+
+ .widget__body {
+ overflow: hidden;
+ }
+
+ &:hover,
+ &:focus-within {
+ .widget__body {
+ overflow-y: auto;
+ }
+ }
+ }
+ }
+ }
+
+ &--expanded & {
+ &__body {
+ max-height: 10000px; // needs a fixed value for transition.
+ margin: var(--spacing-sm) 0;
+ opacity: 1;
+ visibility: visible;
+ transition: all 0.5s ease-in-out 0s, border 0s linear 0s,
+ max-height 0.6s ease-in-out 0s;
+ }
+ }
+}
diff --git a/src/components/molecules/layout/widget.stories.tsx b/src/components/molecules/layout/widget.stories.tsx
new file mode 100644
index 0000000..dd5a30b
--- /dev/null
+++ b/src/components/molecules/layout/widget.stories.tsx
@@ -0,0 +1,117 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import headingButtonStories from '../buttons/heading-button.stories';
+import Widget from './widget';
+
+/**
+ * Widget - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout/Widget',
+ component: Widget,
+ args: {
+ withBorders: false,
+ withScroll: false,
+ },
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The widget body',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the widget wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ expanded: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'The widget state (expanded or collapsed)',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: true },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ level: headingButtonStories.argTypes?.level,
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The widget title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ withBorders: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Define if the content should have borders.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ withScroll: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Define if the widget should be scrollable',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Widget>;
+
+const Template: ComponentStory<typeof Widget> = (args) => <Widget {...args} />;
+
+/**
+ * Widget Stories - Expanded
+ */
+export const Expanded = Template.bind({});
+Expanded.args = {
+ children: 'Widget body',
+ expanded: true,
+ level: 2,
+ title: 'Widget title',
+};
+
+/**
+ * Widget Stories - Collapsed
+ */
+export const Collapsed = Template.bind({});
+Collapsed.args = {
+ children: 'Widget body',
+ expanded: false,
+ level: 2,
+ title: 'Widget title',
+};
diff --git a/src/components/molecules/layout/widget.test.tsx b/src/components/molecules/layout/widget.test.tsx
new file mode 100644
index 0000000..af561ea
--- /dev/null
+++ b/src/components/molecules/layout/widget.test.tsx
@@ -0,0 +1,19 @@
+import { render, screen } from '@test-utils';
+import Widget from './widget';
+
+const children = 'Widget body';
+const title = 'Widget title';
+const titleLevel = 2;
+
+describe('Widget', () => {
+ it('renders the widget title', () => {
+ render(
+ <Widget expanded={true} title={title} level={titleLevel}>
+ {children}
+ </Widget>
+ );
+ expect(
+ screen.getByRole('heading', { level: titleLevel })
+ ).toHaveTextContent(title);
+ });
+});
diff --git a/src/components/molecules/layout/widget.tsx b/src/components/molecules/layout/widget.tsx
new file mode 100644
index 0000000..f50fe80
--- /dev/null
+++ b/src/components/molecules/layout/widget.tsx
@@ -0,0 +1,66 @@
+import { FC, ReactNode, useState } from 'react';
+import HeadingButton, {
+ type HeadingButtonProps,
+} from '../buttons/heading-button';
+import styles from './widget.module.scss';
+
+export type WidgetProps = Pick<
+ HeadingButtonProps,
+ 'expanded' | 'level' | 'title'
+> & {
+ /**
+ * The widget body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames to the widget wrapper.
+ */
+ className?: string;
+ /**
+ * Determine if the widget body should have borders. Default: false.
+ */
+ withBorders?: boolean;
+ /**
+ * Determine if a vertical scrollbar should be displayed. Default: false.
+ */
+ withScroll?: boolean;
+};
+
+/**
+ * Widget component
+ *
+ * Render an expandable widget.
+ */
+const Widget: FC<WidgetProps> = ({
+ children,
+ className = '',
+ expanded = true,
+ level,
+ title,
+ withBorders = false,
+ withScroll = false,
+}) => {
+ const [isExpanded, setIsExpanded] = useState<boolean>(expanded);
+ const stateClass = isExpanded ? 'widget--expanded' : 'widget--collapsed';
+ const bordersClass = withBorders
+ ? 'widget--has-borders'
+ : 'widget--no-borders';
+ const scrollClass = withScroll ? 'widget--has-scroll' : 'widget--no-scroll';
+
+ return (
+ <div
+ className={`${styles.widget} ${styles[bordersClass]} ${styles[stateClass]} ${styles[scrollClass]} ${className}`}
+ >
+ <HeadingButton
+ level={level}
+ title={title}
+ expanded={isExpanded}
+ setExpanded={setIsExpanded}
+ className={styles.widget__header}
+ />
+ <div className={styles.widget__body}>{children}</div>
+ </div>
+ );
+};
+
+export default Widget;
diff --git a/src/components/molecules/modals/modal.module.scss b/src/components/molecules/modals/modal.module.scss
new file mode 100644
index 0000000..8866834
--- /dev/null
+++ b/src/components/molecules/modals/modal.module.scss
@@ -0,0 +1,38 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ padding: var(--spacing-md);
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(4) solid;
+ border-image: radial-gradient(
+ ellipse at top,
+ var(--color-primary-lighter) 20%,
+ var(--color-primary) 100%
+ )
+ 1;
+ box-shadow: fun.convert-px(2) fun.convert-px(-2) fun.convert-px(3)
+ fun.convert-px(-1) var(--color-shadow-dark);
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "sm") {
+ padding: var(--spacing-xs);
+ border-left: none;
+ border-right: none;
+
+ .title {
+ margin-bottom: var(--spacing-2xs);
+ }
+ }
+
+ @include mix.dimensions("sm") {
+ max-width: 35ch;
+ }
+ }
+}
+
+.icon {
+ --icon-size: #{fun.convert-px(30)};
+
+ margin-right: var(--spacing-2xs);
+}
diff --git a/src/components/molecules/modals/modal.stories.tsx b/src/components/molecules/modals/modal.stories.tsx
new file mode 100644
index 0000000..f6dd364
--- /dev/null
+++ b/src/components/molecules/modals/modal.stories.tsx
@@ -0,0 +1,96 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Modal from './modal';
+
+/**
+ * Widget - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Modals/Modal',
+ component: Modal,
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The modal body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ headingClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal heading.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ icon: {
+ control: {
+ type: 'select',
+ },
+ description: 'The title icon.',
+ options: ['', 'cogs', 'search'],
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The modal title.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Modal>;
+
+const Template: ComponentStory<typeof Modal> = (args) => <Modal {...args} />;
+
+/**
+ * Modal Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ children:
+ 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.',
+};
+
+/**
+ * Modal Stories - With title
+ */
+export const WithTitle = Template.bind({});
+WithTitle.args = {
+ children:
+ 'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.',
+ title: 'Alias praesentium corporis',
+};
diff --git a/src/components/molecules/modals/modal.test.tsx b/src/components/molecules/modals/modal.test.tsx
new file mode 100644
index 0000000..9a0e237
--- /dev/null
+++ b/src/components/molecules/modals/modal.test.tsx
@@ -0,0 +1,18 @@
+import { render, screen } from '@test-utils';
+import Modal from './modal';
+
+const title = 'A custom title';
+const children =
+ 'Labore ullam delectus sit modi quam dolores. Ratione id sint aliquid facilis ipsum. Unde necessitatibus provident minus.';
+
+describe('Modal', () => {
+ it('renders a title', () => {
+ render(<Modal title={title}>{children}</Modal>);
+ expect(screen.getByText(title)).toBeInTheDocument();
+ });
+
+ it('renders the modal body', () => {
+ render(<Modal title={title}>{children}</Modal>);
+ expect(screen.getByText(children)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/modals/modal.tsx b/src/components/molecules/modals/modal.tsx
new file mode 100644
index 0000000..58f5fa0
--- /dev/null
+++ b/src/components/molecules/modals/modal.tsx
@@ -0,0 +1,81 @@
+import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
+import { type CogProps } from '@components/atoms/icons/cog';
+import { type MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
+import dynamic from 'next/dynamic';
+import { FC, ReactNode } from 'react';
+import styles from './modal.module.scss';
+
+export type Icons = 'cogs' | 'search';
+
+export type ModalProps = {
+ /**
+ * The modal body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames.
+ */
+ className?: string;
+ /**
+ * Set additional classnames to the heading.
+ */
+ headingClassName?: HeadingProps['className'];
+ /**
+ * A icon to illustrate the modal.
+ */
+ icon?: Icons;
+ /**
+ * The modal title.
+ */
+ title?: string;
+};
+
+const CogIcon = dynamic<CogProps>(() => import('@components/atoms/icons/cog'), {
+ ssr: false,
+});
+const SearchIcon = dynamic<MagnifyingGlassProps>(
+ () => import('@components/atoms/icons/magnifying-glass'),
+ { ssr: false }
+);
+
+/**
+ * Modal component
+ *
+ * Render a modal component with an optional title and icon.
+ */
+const Modal: FC<ModalProps> = ({
+ children,
+ className = '',
+ headingClassName = '',
+ icon,
+ title,
+}) => {
+ const getIcon = (id: Icons) => {
+ switch (id) {
+ case 'cogs':
+ return <CogIcon />;
+ case 'search':
+ return <SearchIcon />;
+ default:
+ return <></>;
+ }
+ };
+
+ return (
+ <div className={`${styles.wrapper} ${className}`}>
+ {title && (
+ <Heading
+ isFake={true}
+ level={3}
+ className={`${styles.title} ${headingClassName}`}
+ >
+ {icon && <span className={styles.icon}>{getIcon(icon)}</span>}
+ {title}
+ </Heading>
+ )}
+ {children}
+ </div>
+ );
+};
+
+export default Modal;
diff --git a/src/components/molecules/modals/tooltip.module.scss b/src/components/molecules/modals/tooltip.module.scss
new file mode 100644
index 0000000..94aa3dd
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.module.scss
@@ -0,0 +1,46 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ --title-height: #{fun.convert-px(40)};
+
+ margin-top: calc(var(--title-height) / 2);
+ padding: calc((var(--title-height) / 2) + var(--spacing-sm)) var(--spacing-sm)
+ var(--spacing-sm);
+ position: relative;
+ background: var(--color-bg);
+ border: fun.convert-px(2) solid var(--color-primary-dark);
+ border-radius: fun.convert-px(3);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow),
+ fun.convert-px(2) fun.convert-px(2) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow-light);
+}
+
+.title {
+ display: flex;
+ align-items: center;
+ height: var(--title-height);
+ padding-right: var(--spacing-xs);
+ position: absolute;
+ top: calc(var(--title-height) / -2);
+ left: var(--spacing-xs);
+ background: var(--color-bg);
+ border: fun.convert-px(1) solid var(--color-primary-dark);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow);
+ color: var(--color-primary-darker);
+ font-size: var(--font-size-sm);
+ font-variant: small-caps;
+ font-weight: 500;
+}
+
+.icon {
+ display: flex;
+ align-items: center;
+ height: var(--title-height);
+ margin-right: var(--spacing-xs);
+ padding: 0 var(--spacing-2xs);
+ background: var(--color-primary-dark);
+ border: fun.convert-px(1) solid var(--color-primary-dark);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) 0 0 var(--color-shadow);
+ color: var(--color-fg-inverted);
+ font-weight: 600;
+}
diff --git a/src/components/molecules/modals/tooltip.stories.tsx b/src/components/molecules/modals/tooltip.stories.tsx
new file mode 100644
index 0000000..06a4855
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.stories.tsx
@@ -0,0 +1,70 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Tooltip from './tooltip';
+
+/**
+ * Tooltip - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Modals/Tooltip',
+ component: Tooltip,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ content: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ icon: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip icon.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The tooltip title',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Tooltip>;
+
+const Template: ComponentStory<typeof Tooltip> = (args) => (
+ <Tooltip {...args} />
+);
+
+/**
+ * Tooltip Stories - Help
+ */
+export const Help = Template.bind({});
+Help.args = {
+ content:
+ 'Minima tempora fuga omnis ratione doloribus ut. Totam ea vitae consequatur. Fuga hic ipsum. In non debitis ex assumenda ut dicta. Sit ut maxime eligendi est.',
+ icon: '?',
+ title: 'Laborum enim vero',
+};
diff --git a/src/components/molecules/modals/tooltip.test.tsx b/src/components/molecules/modals/tooltip.test.tsx
new file mode 100644
index 0000000..24f20d8
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.test.tsx
@@ -0,0 +1,24 @@
+import { render, screen } from '@test-utils';
+import Tooltip from './tooltip';
+
+const title = 'Illum eum at';
+const content =
+ 'Non accusantium ad. Est et impedit iste animi voluptas cum accusamus accusantium. Repellat ut sint pariatur cumque cupiditate. Animi occaecati odio ut debitis ipsam similique. Repudiandae aut earum occaecati consequatur laborum ut nobis iusto. Adipisci laboriosam id.';
+const icon = '?';
+
+describe('Tooltip', () => {
+ it('renders a title', () => {
+ render(<Tooltip title={title} content={content} icon={icon} />);
+ expect(screen.getByText(title)).toBeInTheDocument();
+ });
+
+ it('renders an explanation', () => {
+ render(<Tooltip title={title} content={content} icon={icon} />);
+ expect(screen.getByText(content)).toBeInTheDocument();
+ });
+
+ it('renders an icon', () => {
+ render(<Tooltip title={title} content={content} icon={icon} />);
+ expect(screen.getByText(icon)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx
new file mode 100644
index 0000000..efb3009
--- /dev/null
+++ b/src/components/molecules/modals/tooltip.tsx
@@ -0,0 +1,60 @@
+import List, { type ListItem } from '@components/atoms/lists/list';
+import { forwardRef, ForwardRefRenderFunction, ReactNode } from 'react';
+import styles from './tooltip.module.scss';
+
+export type TooltipProps = {
+ /**
+ * Set additional classnames to the tooltip wrapper.
+ */
+ className?: string;
+ /**
+ * The tooltip body.
+ */
+ content: string | string[];
+ /**
+ * An icon to illustrate tooltip content.
+ */
+ icon: ReactNode;
+ /**
+ * The tooltip title.
+ */
+ title: string;
+};
+
+/**
+ * Tooltip component
+ *
+ * Render a tooltip modal.
+ */
+const Tooltip: ForwardRefRenderFunction<HTMLDivElement, TooltipProps> = (
+ { className = '', content, icon, title },
+ ref
+) => {
+ /**
+ * Format an array of strings to an array of object with id and value.
+ *
+ * @param {string[]} array - An array of strings.
+ * @returns {ListItem[]} The array formatted to be used as list items.
+ */
+ const getListItems = (array: string[]): ListItem[] => {
+ return array.map((string, index) => {
+ return { id: `item-${index}`, value: string };
+ });
+ };
+
+ return (
+ <div className={`${styles.wrapper} ${className}`} ref={ref}>
+ <div className={styles.title}>
+ <span className={styles.icon}>{icon}</span>
+ {title}
+ </div>
+ {Array.isArray(content) ? (
+ <List items={getListItems(content)} />
+ ) : (
+ content
+ )}
+ </div>
+ );
+};
+
+export default forwardRef(Tooltip);
diff --git a/src/components/Breadcrumb/Breadcrumb.module.scss b/src/components/molecules/nav/breadcrumb.module.scss
index b8fadf8..c26f60a 100644
--- a/src/components/Breadcrumb/Breadcrumb.module.scss
+++ b/src/components/molecules/nav/breadcrumb.module.scss
@@ -1,22 +1,12 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
@use "@styles/abstracts/placeholders";
-.wrapper {
- composes: grid from "@styles/layout/_grid.scss";
- padding: var(--spacing-md) 0;
-}
-
.list {
@extend %reset-ordered-list;
- grid-column: 2;
display: flex;
flex-flow: row wrap;
align-items: center;
gap: var(--spacing-2xs);
- margin: 0;
- font-size: var(--font-size-sm);
}
.item {
diff --git a/src/components/molecules/nav/breadcrumb.stories.tsx b/src/components/molecules/nav/breadcrumb.stories.tsx
new file mode 100644
index 0000000..cf67e60
--- /dev/null
+++ b/src/components/molecules/nav/breadcrumb.stories.tsx
@@ -0,0 +1,81 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Breadcrumb from './breadcrumb';
+
+/**
+ * Breadcrumb - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Navigation/Breadcrumb',
+ component: Breadcrumb,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ table: {
+ category: 'Styles',
+ },
+ description: 'Set additional classnames to the nav element.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ itemClassName: {
+ control: {
+ type: 'text',
+ },
+ table: {
+ category: 'Styles',
+ },
+ description: 'Set additional classnames to the breadcrumb items.',
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ description: 'The breadcrumb items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof Breadcrumb>;
+
+const Template: ComponentStory<typeof Breadcrumb> = (args) => (
+ <Breadcrumb {...args} />
+);
+
+/**
+ * Breadcrumb Stories - One item
+ */
+export const OneItem = Template.bind({});
+OneItem.args = {
+ items: [{ id: 'home', url: '#', name: 'Home' }],
+};
+
+/**
+ * Breadcrumb Stories - Two items
+ */
+export const TwoItems = Template.bind({});
+TwoItems.args = {
+ items: [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'blog', url: '#', name: 'Blog' },
+ ],
+};
+
+/**
+ * Breadcrumb Stories - Three items
+ */
+export const ThreeItems = Template.bind({});
+ThreeItems.args = {
+ items: [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'blog', url: '#', name: 'Blog' },
+ { id: 'post1', url: '#', name: 'A Post' },
+ ],
+};
diff --git a/src/components/molecules/nav/breadcrumb.test.tsx b/src/components/molecules/nav/breadcrumb.test.tsx
new file mode 100644
index 0000000..43220c9
--- /dev/null
+++ b/src/components/molecules/nav/breadcrumb.test.tsx
@@ -0,0 +1,15 @@
+import { render, screen } from '@test-utils';
+import Breadcrumb, { type BreadcrumbItem } from './breadcrumb';
+
+const items: BreadcrumbItem[] = [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'blog', url: '#', name: 'Blog' },
+ { id: 'post1', url: '#', name: 'A Post' },
+];
+
+describe('Breadcrumb', () => {
+ it('renders a navigation', () => {
+ render(<Breadcrumb items={items} />);
+ expect(screen.getByRole('navigation')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/molecules/nav/breadcrumb.tsx b/src/components/molecules/nav/breadcrumb.tsx
new file mode 100644
index 0000000..d184d65
--- /dev/null
+++ b/src/components/molecules/nav/breadcrumb.tsx
@@ -0,0 +1,127 @@
+import Link from '@components/atoms/links/link';
+import { settings } from '@utils/config';
+import Script from 'next/script';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import { BreadcrumbList, ListItem, WithContext } from 'schema-dts';
+import styles from './breadcrumb.module.scss';
+
+export type BreadcrumbItem = {
+ /**
+ * The item id.
+ */
+ id: string;
+ /**
+ * The item URL.
+ */
+ url: string;
+ /**
+ * The item name.
+ */
+ name: string;
+};
+
+export type BreadcrumbProps = {
+ /**
+ * Set additional classnames to the nav element.
+ */
+ className?: string;
+ /**
+ * Set additional classnames to the breadcrumb items.
+ */
+ itemClassName?: string;
+ /**
+ * The breadcrumb items
+ */
+ items: BreadcrumbItem[];
+};
+
+/**
+ * Breadcrumb component
+ *
+ * Render a breadcrumb navigation.
+ */
+const Breadcrumb: FC<BreadcrumbProps> = ({
+ itemClassName = '',
+ items,
+ ...props
+}) => {
+ const intl = useIntl();
+
+ const ariaLabel = intl.formatMessage({
+ defaultMessage: 'Breadcrumb',
+ description: 'Breadcrumb: an accessible name for the breadcrumb nav.',
+ id: '28nnDY',
+ });
+
+ /**
+ * Retrieve the breadcrumb list items.
+ *
+ * @param {BreadcrumbItem[]} list - The breadcrumb items.
+ * @returns {JSX.Element[]} The list items.
+ */
+ const getListItems = (list: BreadcrumbItem[]): JSX.Element[] => {
+ return list.map((item, index) => {
+ const isLastItem = index === list.length - 1;
+ const itemStyles = isLastItem
+ ? `${styles.item} screen-reader-text`
+ : styles.item;
+
+ return (
+ <li key={item.id} className={`${itemStyles} ${itemClassName}`}>
+ {isLastItem ? item.name : <Link href={item.url}>{item.name}</Link>}
+ </li>
+ );
+ });
+ };
+
+ /**
+ * Retrieve the breadcrumb list items with Schema.org format.
+ *
+ * @param {BreadcrumbItem[]} list - The breadcrumb items.
+ * @returns {ListItem[]} An array of list items using Schema.org format.
+ */
+ const getSchemaItems = (list: BreadcrumbItem[]): ListItem[] => {
+ const schemaItems: ListItem[] = [];
+
+ list.forEach((item, index) => {
+ schemaItems.push({
+ '@type': 'ListItem',
+ position: index + 1,
+ name: item.name,
+ item: item.url,
+ });
+ });
+
+ return schemaItems;
+ };
+
+ const schemaJsonLd: WithContext<BreadcrumbList> = {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ '@id': `${settings.url}/#breadcrumb`,
+ itemListElement: getSchemaItems(items),
+ };
+
+ return (
+ <>
+ <Script
+ id="schema-breadcrumb"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <nav aria-label={ariaLabel} {...props}>
+ <span className="screen-reader-text">
+ {intl.formatMessage({
+ defaultMessage: 'You are here:',
+ description: 'Breadcrumb: You are here prefix',
+ id: '16zl9Z',
+ })}
+ </span>
+ <ol className={styles.list}>{getListItems(items)}</ol>
+ </nav>
+ </>
+ );
+};
+
+export default Breadcrumb;
diff --git a/src/components/molecules/nav/nav.module.scss b/src/components/molecules/nav/nav.module.scss
new file mode 100644
index 0000000..9c0f6de
--- /dev/null
+++ b/src/components/molecules/nav/nav.module.scss
@@ -0,0 +1,22 @@
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.nav {
+ &__list {
+ @extend %reset-list;
+
+ display: flex;
+ flex-flow: row wrap;
+ gap: var(--spacing-2xs);
+ align-items: center;
+ }
+
+ &--footer & {
+ &__item:not(:first-child) {
+ &::before {
+ content: "\2022";
+ margin-right: var(--spacing-2xs);
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/nav/nav.stories.tsx b/src/components/molecules/nav/nav.stories.tsx
new file mode 100644
index 0000000..f3a29a6
--- /dev/null
+++ b/src/components/molecules/nav/nav.stories.tsx
@@ -0,0 +1,107 @@
+import Envelop from '@components/atoms/icons/envelop';
+import Home from '@components/atoms/icons/home';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import NavComponent, { type NavItem } from './nav';
+
+/**
+ * Nav - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Navigation/Nav',
+ component: NavComponent,
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible name for the navigation.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the navigation wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ control: {
+ type: null,
+ },
+ description: 'The nav items.',
+ type: {
+ name: 'other',
+ required: true,
+ value: '',
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The navigation kind.',
+ options: ['main', 'footer'],
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ listClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the navigation list.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof NavComponent>;
+
+const Template: ComponentStory<typeof NavComponent> = (args) => (
+ <NavComponent {...args} />
+);
+
+const MainNavItems: NavItem[] = [
+ { id: 'homeLink', href: '/', label: 'Home', logo: <Home /> },
+ { id: 'contactLink', href: '/contact', label: 'Contact', logo: <Envelop /> },
+];
+
+const FooterNavItems: NavItem[] = [
+ { id: 'contactLink', href: '/contact', label: 'Contact' },
+ { id: 'legalLink', href: '/legal-notice', label: 'Legal notice' },
+];
+
+/**
+ * Nav Stories - Main navigation
+ */
+export const MainNav = Template.bind({});
+MainNav.args = {
+ items: MainNavItems,
+ kind: 'main',
+};
+
+/**
+ * Nav Stories - Footer navigation
+ */
+export const FooterNav = Template.bind({});
+FooterNav.args = {
+ items: FooterNavItems,
+ kind: 'footer',
+};
diff --git a/src/components/molecules/nav/nav.test.tsx b/src/components/molecules/nav/nav.test.tsx
new file mode 100644
index 0000000..183ca0b
--- /dev/null
+++ b/src/components/molecules/nav/nav.test.tsx
@@ -0,0 +1,28 @@
+import Envelop from '@components/atoms/icons/envelop';
+import Home from '@components/atoms/icons/home';
+import { render, screen } from '@test-utils';
+import Nav, { type NavItem } from './nav';
+
+const navItems: NavItem[] = [
+ { id: 'homeLink', href: '/', label: 'Home', logo: <Home /> },
+ { id: 'contactLink', href: '/contact', label: 'Contact', logo: <Envelop /> },
+];
+
+describe('Nav', () => {
+ it('renders a main navigation', () => {
+ render(<Nav kind="main" items={navItems} />);
+ expect(screen.getByRole('navigation')).toHaveClass('nav--main');
+ });
+
+ it('renders a footer navigation', () => {
+ render(<Nav kind="footer" items={navItems} />);
+ expect(screen.getByRole('navigation')).toHaveClass('nav--footer');
+ });
+
+ it('renders navigation links', () => {
+ render(<Nav kind="main" items={navItems} />);
+ expect(
+ screen.getByRole('link', { name: navItems[0].label })
+ ).toHaveAttribute('href', navItems[0].href);
+ });
+});
diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav.tsx
new file mode 100644
index 0000000..581f813
--- /dev/null
+++ b/src/components/molecules/nav/nav.tsx
@@ -0,0 +1,85 @@
+import Link from '@components/atoms/links/link';
+import NavLink from '@components/atoms/links/nav-link';
+import { FC, ReactNode } from 'react';
+import styles from './nav.module.scss';
+
+export type NavItem = {
+ /**
+ * The item id.
+ */
+ id: string;
+ /**
+ * The item link.
+ */
+ href: string;
+ /**
+ * The item name.
+ */
+ label: string;
+ /**
+ * The item logo.
+ */
+ logo?: ReactNode;
+};
+
+export type NavProps = {
+ /**
+ * An accessible name.
+ */
+ 'aria-label'?: string;
+ /**
+ * Set additional classnames to the navigation wrapper.
+ */
+ className?: string;
+ /**
+ * The navigation items.
+ */
+ items: NavItem[];
+ /**
+ * The navigation kind.
+ */
+ kind: 'main' | 'footer';
+ /**
+ * Set additional classnames to the navigation list.
+ */
+ listClassName?: string;
+};
+
+/**
+ * Nav component
+ *
+ * Render the nav links.
+ */
+const Nav: FC<NavProps> = ({
+ className = '',
+ items,
+ kind,
+ listClassName = '',
+ ...props
+}) => {
+ const kindClass = `nav--${kind}`;
+
+ /**
+ * Get the nav items.
+ * @returns {JSX.Element[]} An array of nav items.
+ */
+ const getItems = (): JSX.Element[] => {
+ return items.map(({ id, href, label, logo }) => (
+ <li key={id} className={styles.nav__item}>
+ {kind === 'main' ? (
+ <NavLink href={href} label={label} logo={logo} />
+ ) : (
+ <Link href={href}>{label}</Link>
+ )}
+ </li>
+ ));
+ };
+
+ return (
+ <nav className={`${styles[kindClass]} ${className}`} {...props}>
+ <ul className={`${styles.nav__list} ${listClassName}`}>{getItems()}</ul>
+ </nav>
+ );
+};
+
+export default Nav;
diff --git a/src/components/molecules/nav/pagination.module.scss b/src/components/molecules/nav/pagination.module.scss
new file mode 100644
index 0000000..56c5bfc
--- /dev/null
+++ b/src/components/molecules/nav/pagination.module.scss
@@ -0,0 +1,51 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/placeholders";
+
+.wrapper {
+ .list {
+ @extend %flex-list;
+
+ align-items: stretch;
+ justify-content: center;
+ position: relative;
+ row-gap: var(--spacing-xs);
+ column-gap: var(--spacing-sm);
+
+ &--pages {
+ column-gap: var(--spacing-2xs);
+ margin-bottom: var(--spacing-sm);
+ }
+ }
+
+ .link {
+ height: 100%;
+ min-width: 5ch;
+ min-height: 6ex;
+ position: relative;
+
+ &:not(&--disabled) {
+ &:hover,
+ &:focus {
+ z-index: 3;
+ }
+ }
+
+ &--number {
+ padding: 0;
+ }
+
+ &--disabled {
+ display: flex;
+ place-content: center;
+ align-items: center;
+ background: var(--color-bg);
+ border: fun.convert-px(3) solid var(--color-primary-darker);
+ border-radius: fun.convert-px(5);
+ color: var(--color-primary-darker);
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ text-decoration: underline transparent 0;
+ transform: scale(var(--scale-down, 0.94));
+ }
+ }
+}
diff --git a/src/components/molecules/nav/pagination.stories.tsx b/src/components/molecules/nav/pagination.stories.tsx
new file mode 100644
index 0000000..2e86db4
--- /dev/null
+++ b/src/components/molecules/nav/pagination.stories.tsx
@@ -0,0 +1,171 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import PaginationComponent from './pagination';
+
+/**
+ * Pagination - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Navigation/Pagination',
+ component: PaginationComponent,
+ args: {
+ baseUrl: '/page/',
+ siblings: 1,
+ },
+ argTypes: {
+ 'aria-label': {
+ control: {
+ type: 'text',
+ },
+ description: 'An accessible name for the pagination.',
+ table: {
+ category: 'Accessibility',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ baseUrl: {
+ control: {
+ type: 'text',
+ },
+ description: 'The url prefix.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: '/page/' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the pagination wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ current: {
+ control: {
+ type: 'number',
+ },
+ description: 'The current page number.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ perPage: {
+ control: {
+ type: 'number',
+ },
+ description: 'The number of items per page.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ siblings: {
+ control: {
+ type: 'number',
+ },
+ description:
+ 'The number of pages to show next to the current page for one side.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 1 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ total: {
+ control: {
+ type: 'number',
+ },
+ description: 'The total number of items.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof PaginationComponent>;
+
+const Template: ComponentStory<typeof PaginationComponent> = (args) => (
+ <PaginationComponent {...args} />
+);
+
+/**
+ * Pagination Stories - Less than 5 pages
+ */
+export const WithoutDots = Template.bind({});
+WithoutDots.args = {
+ current: 2,
+ perPage: 10,
+ siblings: 2,
+ total: 50,
+};
+
+/**
+ * Pagination Stories - Truncated to the right.
+ */
+export const RightDots = Template.bind({});
+RightDots.args = {
+ current: 2,
+ perPage: 10,
+ siblings: 2,
+ total: 80,
+};
+
+/**
+ * Pagination Stories - Truncated to the left.
+ */
+export const LeftDots = Template.bind({});
+LeftDots.args = {
+ current: 7,
+ perPage: 10,
+ siblings: 2,
+ total: 80,
+};
+
+/**
+ * Pagination Stories - Truncated both sides.
+ */
+export const LeftAndRightDots = Template.bind({});
+LeftAndRightDots.args = {
+ current: 6,
+ perPage: 10,
+ siblings: 2,
+ total: 150,
+};
+
+/**
+ * Pagination Stories - Without previous link
+ */
+export const WithoutPreviousLink = Template.bind({});
+WithoutPreviousLink.args = {
+ current: 1,
+ perPage: 10,
+ siblings: 2,
+ total: 50,
+};
+
+/**
+ * Pagination Stories - Without next link
+ */
+export const WithoutNextLink = Template.bind({});
+WithoutNextLink.args = {
+ current: 5,
+ perPage: 10,
+ siblings: 2,
+ total: 50,
+};
diff --git a/src/components/molecules/nav/pagination.test.tsx b/src/components/molecules/nav/pagination.test.tsx
new file mode 100644
index 0000000..2c4a063
--- /dev/null
+++ b/src/components/molecules/nav/pagination.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from '@test-utils';
+import Pagination from './pagination';
+
+const total = 50;
+const perPage = 10;
+
+describe('Pagination', () => {
+ it('renders previous and next page links', () => {
+ render(<Pagination current={2} total={total} perPage={perPage} />);
+ expect(
+ screen.getByRole('link', { name: /Previous page/i })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('link', { name: /Next page/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the page links except for the current one', () => {
+ render(
+ <Pagination current={2} siblings={2} total={total} perPage={perPage} />
+ );
+ expect(screen.getAllByRole('link', { name: /Page / })).toHaveLength(
+ total / perPage - 1
+ );
+ });
+});
diff --git a/src/components/molecules/nav/pagination.tsx b/src/components/molecules/nav/pagination.tsx
new file mode 100644
index 0000000..934b50a
--- /dev/null
+++ b/src/components/molecules/nav/pagination.tsx
@@ -0,0 +1,220 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import { FC, Fragment, ReactNode } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './pagination.module.scss';
+
+export type PaginationProps = {
+ /**
+ * An accessible name for the pagination.
+ */
+ 'aria-label'?: string;
+ /**
+ * The url part before page number. Default: /page/
+ */
+ baseUrl?: string;
+ /**
+ * Set additional classnames to the pagination wrapper.
+ */
+ className?: string;
+ /**
+ * The current page number.
+ */
+ current: number;
+ /**
+ * The number of items per page.
+ */
+ perPage: number;
+ /**
+ * The number of siblings on one side of the current page. Default: 1.
+ */
+ siblings?: number;
+ /**
+ * The total number of items.
+ */
+ total: number;
+};
+
+/**
+ * Pagination component
+ *
+ * Render a page-based navigation.
+ */
+const Pagination: FC<PaginationProps> = ({
+ baseUrl = '/page/',
+ className = '',
+ current,
+ perPage,
+ siblings = 2,
+ total,
+ ...props
+}) => {
+ const intl = useIntl();
+ const totalPages = Math.round(total / perPage);
+ const hasPreviousPage = current > 1;
+ const previousPageName = intl.formatMessage(
+ {
+ defaultMessage: '{icon} Previous page',
+ description: 'Pagination: previous page link',
+ id: 'aMFqPH',
+ },
+ { icon: '←' }
+ );
+ const previousPageUrl = `${baseUrl}${current - 1}`;
+ const hasNextPage = current < totalPages;
+ const nextPageName = intl.formatMessage(
+ {
+ defaultMessage: 'Next page {icon}',
+ description: 'Pagination: Next page link',
+ id: 'R4yaW6',
+ },
+ { icon: '→' }
+ );
+ const nextPageUrl = `${baseUrl}${current + 1}`;
+
+ /**
+ * Create an array with a range of values from start value to end value.
+ *
+ * @param {number} start - The first value.
+ * @param {number} end - The last value.
+ * @returns {number[]} An array from start value to end value.
+ */
+ const range = (start: number, end: number): number[] => {
+ const length = end - start + 1;
+
+ return Array.from({ length }, (_, index) => index + start);
+ };
+
+ /**
+ * Get the pagination range.
+ *
+ * @param currentPage - The current page number.
+ * @param maxPages - The total pages number.
+ * @returns {(number|string)[]} An array of page numbers with or without dots.
+ */
+ const getPaginationRange = (
+ currentPage: number,
+ maxPages: number
+ ): (number | string)[] => {
+ const dots = '\u2026';
+
+ /**
+ * Show left dots if current page less left siblings is greater than the
+ * first two pages.
+ */
+ const hasLeftDots = currentPage - siblings > 2;
+
+ /**
+ * Show right dots if current page plus right siblings is lower than the
+ * total of pages less the last page.
+ */
+ const hasRightDots = currentPage + siblings < maxPages - 1;
+
+ if (hasLeftDots && hasRightDots) {
+ const middleItems = range(currentPage - siblings, currentPage + siblings);
+ return [1, dots, ...middleItems, dots, maxPages];
+ }
+
+ if (hasLeftDots) {
+ const rightItems = range(currentPage - siblings, maxPages);
+ return [1, dots, ...rightItems];
+ }
+
+ if (hasRightDots) {
+ const leftItems = range(1, currentPage + siblings);
+ return [...leftItems, dots, maxPages];
+ }
+
+ return range(1, maxPages);
+ };
+
+ /**
+ * Get a link or a span wrapped in a list item.
+ *
+ * @param {string} id - The item id.
+ * @param {ReactNode} body - The link body.
+ * @param {string} [link] - An URL.
+ * @returns {JSX.Element} The list item.
+ */
+ const getItem = (id: string, body: ReactNode, link?: string): JSX.Element => {
+ const linkModifier = id.startsWith('page') ? 'link--number' : '';
+ const kind = id === 'previous' || id === 'next' ? 'tertiary' : 'secondary';
+
+ return (
+ <li className={styles.item}>
+ {link ? (
+ <ButtonLink
+ kind={kind}
+ target={link}
+ className={`${styles.link} ${styles[linkModifier]}`}
+ >
+ {body}
+ </ButtonLink>
+ ) : (
+ <span className={`${styles.link} ${styles['link--disabled']}`}>
+ {body}
+ </span>
+ )}
+ </li>
+ );
+ };
+
+ /**
+ * Get the list of pages.
+ *
+ * @param {number} currentPage - The current page number.
+ * @param {number} maxPages - The total of pages.
+ * @returns {JSX.Element[]} The list items.
+ */
+ const getPages = (currentPage: number, maxPages: number): JSX.Element[] => {
+ const pagesRange = getPaginationRange(currentPage, maxPages);
+
+ return pagesRange.map((page, index) => {
+ const id = typeof page === 'string' ? `dots-${index}` : `page-${page}`;
+ const currentPagePrefix = intl.formatMessage({
+ defaultMessage: 'You are here:',
+ description: 'Pagination: current page indication',
+ id: 'yE/Jdz',
+ });
+ const body =
+ typeof page === 'string'
+ ? '\u2026'
+ : intl.formatMessage(
+ {
+ defaultMessage: '<a11y>Page </a11y>{number}',
+ description: 'Pagination: page number',
+ id: 'TSXPzr',
+ },
+ {
+ number: page,
+ a11y: (chunks: ReactNode) => (
+ <span className="screen-reader-text">
+ {page === currentPage && currentPagePrefix}
+ {chunks}
+ </span>
+ ),
+ }
+ );
+ const url =
+ page === currentPage || typeof page === 'string'
+ ? undefined
+ : `${baseUrl}${page}`;
+
+ return <Fragment key={`item-${id}`}>{getItem(id, body, url)}</Fragment>;
+ });
+ };
+
+ return (
+ <nav className={`${styles.wrapper} ${className}`} {...props}>
+ <ul className={`${styles.list} ${styles['list--pages']}`}>
+ {getPages(current, totalPages)}
+ </ul>
+ <ul className={styles.list}>
+ {hasPreviousPage &&
+ getItem('previous', previousPageName, previousPageUrl)}
+ {hasNextPage && getItem('next', nextPageName, nextPageUrl)}
+ </ul>
+ </nav>
+ );
+};
+
+export default Pagination;
diff --git a/src/components/organisms/forms/comment-form.module.scss b/src/components/organisms/forms/comment-form.module.scss
new file mode 100644
index 0000000..f3f2646
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.module.scss
@@ -0,0 +1,8 @@
+.field {
+ width: 100%;
+}
+
+.button {
+ display: block;
+ margin: auto;
+}
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
new file mode 100644
index 0000000..1a9e7b7
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -0,0 +1,123 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CommentForm from './comment-form';
+
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
+/**
+ * CommentForm - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Forms',
+ component: CommentForm,
+ args: {
+ saveComment,
+ titleAlignment: 'left',
+ titleLevel: 2,
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the form wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ Notice: {
+ control: {
+ type: null,
+ },
+ description: 'A component to display a success or error message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ parentId: {
+ control: {
+ type: null,
+ },
+ description: 'The parent id if it is a reply.',
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ saveComment: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to process the comment form data.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The form title.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ titleAlignment: {
+ control: {
+ type: 'select',
+ },
+ description: 'The heading alignment.',
+ options: ['center', 'left'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'left' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The title level (hn).',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 2 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof CommentForm>;
+
+const Template: ComponentStory<typeof CommentForm> = (args) => (
+ <CommentForm {...args} />
+);
+
+/**
+ * Forms Stories - Comment
+ */
+export const Comment = Template.bind({});
diff --git a/src/components/organisms/forms/comment-form.test.tsx b/src/components/organisms/forms/comment-form.test.tsx
new file mode 100644
index 0000000..c67ad6b
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.test.tsx
@@ -0,0 +1,23 @@
+import { render, screen } from '@test-utils';
+import CommentForm from './comment-form';
+
+const saveComment = async () => {
+ /** Do nothing. */
+};
+const title = 'Cum voluptas voluptatibus';
+
+describe('CommentForm', () => {
+ it('renders a form', () => {
+ render(<CommentForm saveComment={saveComment} />);
+ expect(screen.getByRole('form')).toBeInTheDocument();
+ });
+
+ it('renders an optional title', () => {
+ render(
+ <CommentForm saveComment={saveComment} title={title} titleLevel={2} />
+ );
+ expect(
+ screen.getByRole('heading', { level: 2, name: title })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
new file mode 100644
index 0000000..b2c725f
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -0,0 +1,193 @@
+import Button from '@components/atoms/buttons/button';
+import Form, { type FormProps } from '@components/atoms/forms/form';
+import Heading, {
+ type HeadingProps,
+ type HeadingLevel,
+} from '@components/atoms/headings/heading';
+import Spinner from '@components/atoms/loaders/spinner';
+import LabelledField from '@components/molecules/forms/labelled-field';
+import { FC, ReactNode, useState } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './comment-form.module.scss';
+
+export type CommentFormData = {
+ comment: string;
+ email: string;
+ name: string;
+ parentId?: number;
+ website?: string;
+};
+
+export type CommentFormProps = Pick<FormProps, 'className'> & {
+ /**
+ * Pass a component to print a success/error message.
+ */
+ Notice?: ReactNode;
+ /**
+ * The comment parent id.
+ */
+ parentId?: number;
+ /**
+ * A callback function to save comment. It takes a function as parameter to
+ * reset the form.
+ */
+ saveComment: (data: CommentFormData, reset: () => void) => Promise<void>;
+ /**
+ * The form title.
+ */
+ title?: string;
+ /**
+ * The form title alignment. Default: left.
+ */
+ titleAlignment?: HeadingProps['alignment'];
+ /**
+ * The title level. Default: 2.
+ */
+ titleLevel?: HeadingLevel;
+};
+
+const CommentForm: FC<CommentFormProps> = ({
+ Notice,
+ parentId,
+ saveComment,
+ title,
+ titleAlignment,
+ titleLevel = 2,
+ ...props
+}) => {
+ const intl = useIntl();
+ const [name, setName] = useState<string>('');
+ const [email, setEmail] = useState<string>('');
+ const [website, setWebsite] = useState<string>('');
+ const [comment, setComment] = useState<string>('');
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+ /**
+ * Reset all the form fields.
+ */
+ const resetForm = () => {
+ setName('');
+ setEmail('');
+ setWebsite('');
+ setComment('');
+ setIsSubmitting(false);
+ };
+
+ const nameLabel = intl.formatMessage({
+ defaultMessage: 'Name:',
+ description: 'CommentForm: name label',
+ id: 'ZIrTee',
+ });
+
+ const emailLabel = intl.formatMessage({
+ defaultMessage: 'Email:',
+ description: 'CommentForm: email label',
+ id: 'Bh7z5v',
+ });
+
+ const websiteLabel = intl.formatMessage({
+ defaultMessage: 'Website:',
+ description: 'CommentForm: website label',
+ id: 'u41qSk',
+ });
+
+ const commentLabel = intl.formatMessage({
+ defaultMessage: 'Comment:',
+ description: 'CommentForm: comment label',
+ id: 'A8hGaK',
+ });
+
+ const formTitle = intl.formatMessage({
+ defaultMessage: 'Comment form',
+ description: 'CommentForm: aria label',
+ id: 'dz2kDV',
+ });
+
+ const formAriaLabel = title ? undefined : formTitle;
+ const formId = 'comment-form-title';
+ const formLabelledBy = title ? formId : undefined;
+
+ /**
+ * Handle form submit.
+ */
+ const submitHandler = () => {
+ setIsSubmitting(true);
+ saveComment({ comment, email, name, parentId, website }, resetForm).then(
+ () => setIsSubmitting(false)
+ );
+ };
+
+ return (
+ <Form
+ onSubmit={submitHandler}
+ aria-label={formAriaLabel}
+ aria-labelledby={formLabelledBy}
+ {...props}
+ >
+ {title && (
+ <Heading id={formId} level={titleLevel} alignment={titleAlignment}>
+ {title}
+ </Heading>
+ )}
+ <LabelledField
+ type="text"
+ id="commenter-name"
+ name="commenter-name"
+ label={nameLabel}
+ required={true}
+ value={name}
+ setValue={setName}
+ className={styles.field}
+ />
+ <LabelledField
+ type="email"
+ id="commenter-email"
+ name="commenter-email"
+ label={emailLabel}
+ required={true}
+ value={email}
+ setValue={setEmail}
+ className={styles.field}
+ />
+ <LabelledField
+ type="text"
+ id="commenter-website"
+ name="commenter-website"
+ label={websiteLabel}
+ required={false}
+ value={website}
+ setValue={setWebsite}
+ className={styles.field}
+ />
+ <LabelledField
+ type="textarea"
+ id="commenter-comment"
+ name="commenter-comment"
+ label={commentLabel}
+ required={true}
+ value={comment}
+ setValue={setComment}
+ className={styles.field}
+ />
+ <Button type="submit" kind="primary" className={styles.button}>
+ {intl.formatMessage({
+ defaultMessage: 'Publish',
+ description: 'CommentForm: submit button',
+ id: 'OL0Yzx',
+ })}
+ </Button>
+ {isSubmitting && (
+ <Spinner
+ message={intl.formatMessage({
+ defaultMessage: 'Submitting...',
+ description: 'CommentForm: spinner message on submit',
+ id: 'IY5ew6',
+ })}
+ />
+ )}
+ {Notice}
+ </Form>
+ );
+};
+
+export default CommentForm;
diff --git a/src/components/organisms/forms/contact-form.module.scss b/src/components/organisms/forms/contact-form.module.scss
new file mode 100644
index 0000000..f3f2646
--- /dev/null
+++ b/src/components/organisms/forms/contact-form.module.scss
@@ -0,0 +1,8 @@
+.field {
+ width: 100%;
+}
+
+.button {
+ display: block;
+ margin: auto;
+}
diff --git a/src/components/organisms/forms/contact-form.stories.tsx b/src/components/organisms/forms/contact-form.stories.tsx
new file mode 100644
index 0000000..191d448
--- /dev/null
+++ b/src/components/organisms/forms/contact-form.stories.tsx
@@ -0,0 +1,65 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ContactForm from './contact-form';
+
+/**
+ * ContactForm - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Forms',
+ component: ContactForm,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the form wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ Notice: {
+ control: {
+ type: null,
+ },
+ description: 'A component to display a success or error message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ sendMail: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to process the contact form data.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof ContactForm>;
+
+const Template: ComponentStory<typeof ContactForm> = (args) => (
+ <ContactForm {...args} />
+);
+
+/**
+ * Forms Stories - Contact
+ */
+export const Contact = Template.bind({});
+Contact.args = {
+ sendMail: async (_data, reset: () => void) => {
+ reset();
+ },
+};
diff --git a/src/components/organisms/forms/contact-form.test.tsx b/src/components/organisms/forms/contact-form.test.tsx
new file mode 100644
index 0000000..6225fa9
--- /dev/null
+++ b/src/components/organisms/forms/contact-form.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen } from '@test-utils';
+import ContactForm from './contact-form';
+
+const props = {
+ sendMail: async () => {
+ /** Do nothing. */
+ },
+};
+
+describe('ContactForm', () => {
+ it('renders a contact form', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('form', { name: 'Contact form' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a name field', () => {
+ render(<ContactForm {...props} />);
+ expect(screen.getByRole('textbox', { name: /^Name:/ })).toBeInTheDocument();
+ });
+
+ it('renders an email field', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('textbox', { name: /^Email:/ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders an object field', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('textbox', { name: /^Object:/ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a message field', () => {
+ render(<ContactForm {...props} />);
+ expect(
+ screen.getByRole('textbox', { name: /^Message:/ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a submit button', () => {
+ render(<ContactForm {...props} />);
+ expect(screen.getByRole('button', { name: /^Send/ })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/forms/contact-form.tsx b/src/components/organisms/forms/contact-form.tsx
new file mode 100644
index 0000000..912402c
--- /dev/null
+++ b/src/components/organisms/forms/contact-form.tsx
@@ -0,0 +1,158 @@
+import Button from '@components/atoms/buttons/button';
+import Form from '@components/atoms/forms/form';
+import Spinner from '@components/atoms/loaders/spinner';
+import LabelledField from '@components/molecules/forms/labelled-field';
+import { FC, ReactNode, useState } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './contact-form.module.scss';
+
+export type ContactFormData = {
+ email: string;
+ message: string;
+ name: string;
+ subject: string;
+};
+
+export type ContactFormProps = {
+ /**
+ * Set additional classnames to the form wrapper.
+ */
+ className?: string;
+ /**
+ * Pass a component to print a success/error message.
+ */
+ Notice?: ReactNode;
+ /**
+ * A callback function to send mail.
+ */
+ sendMail: (data: ContactFormData, reset: () => void) => Promise<void>;
+};
+
+/**
+ * ContactForm component
+ *
+ * Render a contact form.
+ */
+const ContactForm: FC<ContactFormProps> = ({
+ className = '',
+ Notice,
+ sendMail,
+}) => {
+ const intl = useIntl();
+ const [name, setName] = useState<string>('');
+ const [email, setEmail] = useState<string>('');
+ const [object, setObject] = useState<string>('');
+ const [message, setMessage] = useState<string>('');
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+ /**
+ * Reset all the form fields.
+ */
+ const resetForm = () => {
+ setName('');
+ setEmail('');
+ setObject('');
+ setMessage('');
+ setIsSubmitting(false);
+ };
+
+ const formName = intl.formatMessage({
+ defaultMessage: 'Contact form',
+ description: 'ContactForm: form accessible name',
+ id: 'HFdzae',
+ });
+
+ const nameLabel = intl.formatMessage({
+ defaultMessage: 'Name:',
+ description: 'ContactForm: name label',
+ id: '1dCuCx',
+ });
+
+ const emailLabel = intl.formatMessage({
+ defaultMessage: 'Email:',
+ description: 'ContactForm: email label',
+ id: 'w4B5PA',
+ });
+
+ const objectLabel = intl.formatMessage({
+ defaultMessage: 'Object:',
+ description: 'ContactForm: object label',
+ id: 's8/tyz',
+ });
+
+ const messageLabel = intl.formatMessage({
+ defaultMessage: 'Message:',
+ description: 'ContactForm: message label',
+ id: 'yN5P+m',
+ });
+
+ const submitHandler = async () => {
+ setIsSubmitting(true);
+ sendMail({ email, message, name, subject: object }, resetForm).then(() =>
+ setIsSubmitting(false)
+ );
+ };
+
+ return (
+ <Form aria-label={formName} onSubmit={submitHandler} className={className}>
+ <LabelledField
+ type="text"
+ id="contact-name"
+ name="contact-name"
+ label={nameLabel}
+ required={true}
+ value={name}
+ setValue={setName}
+ className={styles.field}
+ />
+ <LabelledField
+ type="email"
+ id="contact-email"
+ name="contact-email"
+ label={emailLabel}
+ required={true}
+ value={email}
+ setValue={setEmail}
+ className={styles.field}
+ />
+ <LabelledField
+ type="text"
+ id="contact-object"
+ name="contact-object"
+ label={objectLabel}
+ value={object}
+ setValue={setObject}
+ className={styles.field}
+ />
+ <LabelledField
+ type="textarea"
+ id="contact-message"
+ name="contact-message"
+ label={messageLabel}
+ required={true}
+ value={message}
+ setValue={setMessage}
+ className={styles.field}
+ />
+ <Button type="submit" kind="primary" className={styles.button}>
+ {intl.formatMessage({
+ defaultMessage: 'Send',
+ description: 'ContactForm: send button',
+ id: 'VkAnvv',
+ })}
+ </Button>
+ {isSubmitting && (
+ <Spinner
+ message={intl.formatMessage({
+ defaultMessage: 'Sending mail...',
+ description: 'ContactForm: spinner message on submit',
+ id: 'xaqaYQ',
+ })}
+ />
+ )}
+ {Notice}
+ </Form>
+ );
+};
+
+export default ContactForm;
diff --git a/src/components/organisms/forms/search-form.module.scss b/src/components/organisms/forms/search-form.module.scss
new file mode 100644
index 0000000..1d388a4
--- /dev/null
+++ b/src/components/organisms/forms/search-form.module.scss
@@ -0,0 +1,58 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ max-width: 35ch;
+ }
+ }
+}
+
+.btn {
+ position: absolute;
+ right: 0;
+
+ &__icon {
+ transform: scale(0.85);
+ transition: all 0.3s ease-in-out 0s;
+ }
+
+ &:focus {
+ outline: var(--color-primary-light) solid fun.convert-px(3);
+ }
+
+ &:active {
+ outline: none;
+ }
+
+ &:hover &,
+ &:focus & {
+ &__icon {
+ transform: scale(0.85) rotate(20deg) translateY(#{fun.convert-px(3)});
+ }
+ }
+
+ &:active & {
+ &__icon {
+ transform: scale(0.7);
+ }
+ }
+}
+
+.field {
+ width: 100%;
+ padding-right: var(--spacing-lg);
+
+ &:hover ~ .btn {
+ transform: translate(fun.convert-px(-3), fun.convert-px(-3));
+ }
+
+ &:focus ~ .btn {
+ transform: translate(fun.convert-px(3), fun.convert-px(3));
+ }
+}
diff --git a/src/components/organisms/forms/search-form.stories.tsx b/src/components/organisms/forms/search-form.stories.tsx
new file mode 100644
index 0000000..d8c8e1e
--- /dev/null
+++ b/src/components/organisms/forms/search-form.stories.tsx
@@ -0,0 +1,65 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SearchForm from './search-form';
+
+/**
+ * SearchForm - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Forms',
+ component: SearchForm,
+ args: {
+ hideLabel: false,
+ searchPage: '#',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the form wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ hideLabel: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the input label should be visually hidden.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ searchPage: {
+ control: {
+ type: 'text',
+ },
+ description: 'The search results page url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SearchForm>;
+
+const Template: ComponentStory<typeof SearchForm> = (args) => (
+ <SearchForm {...args} />
+);
+
+/**
+ * Forms Stories - Search
+ */
+export const Search = Template.bind({});
+Search.args = {
+ hideLabel: true,
+};
diff --git a/src/components/organisms/forms/search-form.test.tsx b/src/components/organisms/forms/search-form.test.tsx
new file mode 100644
index 0000000..59a2f68
--- /dev/null
+++ b/src/components/organisms/forms/search-form.test.tsx
@@ -0,0 +1,16 @@
+import { render, screen } from '@test-utils';
+import SearchForm from './search-form';
+
+describe('SearchForm', () => {
+ it('renders a search input', () => {
+ render(<SearchForm searchPage="#" />);
+ expect(
+ screen.getByRole('searchbox', { name: 'Search for:' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a submit button', () => {
+ render(<SearchForm searchPage="#" />);
+ expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/forms/search-form.tsx b/src/components/organisms/forms/search-form.tsx
new file mode 100644
index 0000000..1b5f662
--- /dev/null
+++ b/src/components/organisms/forms/search-form.tsx
@@ -0,0 +1,76 @@
+import Button from '@components/atoms/buttons/button';
+import Form from '@components/atoms/forms/form';
+import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
+import LabelledField, {
+ type LabelledFieldProps,
+} from '@components/molecules/forms/labelled-field';
+import { useRouter } from 'next/router';
+import { forwardRef, ForwardRefRenderFunction, useId, useState } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './search-form.module.scss';
+
+export type SearchFormProps = Pick<LabelledFieldProps, 'hideLabel'> & {
+ /**
+ * The search page url.
+ */
+ searchPage: string;
+};
+
+/**
+ * SearchForm component
+ *
+ * Render a search form.
+ */
+const SearchForm: ForwardRefRenderFunction<
+ HTMLInputElement,
+ SearchFormProps
+> = ({ hideLabel, searchPage }, ref) => {
+ const intl = useIntl();
+ const fieldLabel = intl.formatMessage({
+ defaultMessage: 'Search for:',
+ description: 'SearchForm: field accessible label',
+ id: 'X8oujO',
+ });
+ const buttonLabel = intl.formatMessage({
+ defaultMessage: 'Search',
+ description: 'SearchForm: button accessible name',
+ id: 'WMqQrv',
+ });
+
+ const router = useRouter();
+ const [value, setValue] = useState<string>('');
+
+ const submitHandler = () => {
+ router.push({ pathname: searchPage, query: { s: value } });
+ setValue('');
+ };
+
+ const id = useId();
+
+ return (
+ <Form grouped={false} onSubmit={submitHandler} className={styles.wrapper}>
+ <LabelledField
+ className={styles.field}
+ hideLabel={hideLabel}
+ id={`search-form-${id}`}
+ label={fieldLabel}
+ name="search-form"
+ ref={ref}
+ setValue={setValue}
+ type="search"
+ value={value}
+ />
+ <Button
+ type="submit"
+ kind="neutral"
+ shape="initial"
+ className={styles.btn}
+ aria-label={buttonLabel}
+ >
+ <MagnifyingGlass className={styles.btn__icon} />
+ </Button>
+ </Form>
+ );
+};
+
+export default forwardRef(SearchForm);
diff --git a/src/components/organisms/forms/settings-form.module.scss b/src/components/organisms/forms/settings-form.module.scss
new file mode 100644
index 0000000..a6a2077
--- /dev/null
+++ b/src/components/organisms/forms/settings-form.module.scss
@@ -0,0 +1,11 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.label {
+ margin-right: auto;
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "2xs", "height") {
+ font-size: var(--font-size-sm);
+ }
+ }
+}
diff --git a/src/components/organisms/forms/settings-form.stories.tsx b/src/components/organisms/forms/settings-form.stories.tsx
new file mode 100644
index 0000000..70e1844
--- /dev/null
+++ b/src/components/organisms/forms/settings-form.stories.tsx
@@ -0,0 +1,67 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SettingsForm from './settings-form';
+
+/**
+ * SettingsModal - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Forms',
+ component: SettingsForm,
+ argTypes: {
+ ackeeStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'The local storage key for Ackee setting.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ motionStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'The local storage key for reduced motion setting.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ tooltipClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof SettingsForm>;
+
+const Template: ComponentStory<typeof SettingsForm> = (args) => (
+ <SettingsForm {...args} />
+);
+
+/**
+ * Form Stories - Settings
+ */
+export const Settings = Template.bind({});
diff --git a/src/components/organisms/forms/settings-form.test.tsx b/src/components/organisms/forms/settings-form.test.tsx
new file mode 100644
index 0000000..43d546e
--- /dev/null
+++ b/src/components/organisms/forms/settings-form.test.tsx
@@ -0,0 +1,67 @@
+import { render, screen } from '@test-utils';
+import SettingsForm from './settings-form';
+
+const ackeeStorageKey = 'ackee-tracking';
+const motionStorageKey = 'reduce-motion';
+
+describe('SettingsForm', () => {
+ it('renders a form', () => {
+ render(
+ <SettingsForm
+ ackeeStorageKey={ackeeStorageKey}
+ motionStorageKey={motionStorageKey}
+ />
+ );
+ expect(
+ screen.getByRole('form', { name: /^Settings form/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a theme toggle setting', () => {
+ render(
+ <SettingsForm
+ ackeeStorageKey={ackeeStorageKey}
+ motionStorageKey={motionStorageKey}
+ />
+ );
+ expect(
+ screen.getByRole('checkbox', { name: /^Theme:/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a code blocks toggle setting', () => {
+ render(
+ <SettingsForm
+ ackeeStorageKey={ackeeStorageKey}
+ motionStorageKey={motionStorageKey}
+ />
+ );
+ expect(
+ screen.getByRole('checkbox', { name: /^Code blocks:/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a motion setting', () => {
+ render(
+ <SettingsForm
+ ackeeStorageKey={ackeeStorageKey}
+ motionStorageKey={motionStorageKey}
+ />
+ );
+ expect(
+ screen.getByRole('checkbox', { name: /^Animations:/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a Ackee setting', () => {
+ render(
+ <SettingsForm
+ ackeeStorageKey={ackeeStorageKey}
+ motionStorageKey={motionStorageKey}
+ />
+ );
+ expect(
+ screen.getByRole('combobox', { name: /^Tracking:/i })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/forms/settings-form.tsx b/src/components/organisms/forms/settings-form.tsx
new file mode 100644
index 0000000..c897fa5
--- /dev/null
+++ b/src/components/organisms/forms/settings-form.tsx
@@ -0,0 +1,56 @@
+import Form from '@components/atoms/forms/form';
+import AckeeSelect, {
+ type AckeeSelectProps,
+} from '@components/molecules/forms/ackee-select';
+import MotionToggle, {
+ MotionToggleProps,
+} from '@components/molecules/forms/motion-toggle';
+import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle';
+import ThemeToggle from '@components/molecules/forms/theme-toggle';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './settings-form.module.scss';
+
+export type SettingsFormProps = Pick<AckeeSelectProps, 'tooltipClassName'> & {
+ /**
+ * The local storage key for Ackee settings.
+ */
+ ackeeStorageKey: AckeeSelectProps['storageKey'];
+ /**
+ * The local storage key for Reduce motion settings.
+ */
+ motionStorageKey: MotionToggleProps['storageKey'];
+};
+
+const SettingsForm: FC<SettingsFormProps> = ({
+ ackeeStorageKey,
+ motionStorageKey,
+ tooltipClassName,
+}) => {
+ const intl = useIntl();
+ const ariaLabel = intl.formatMessage({
+ defaultMessage: 'Settings form',
+ id: 'gX+YVy',
+ description: 'SettingsForm: an accessible form name',
+ });
+
+ return (
+ <Form aria-label={ariaLabel} onSubmit={() => null}>
+ <ThemeToggle labelClassName={styles.label} />
+ <PrismThemeToggle labelClassName={styles.label} />
+ <MotionToggle
+ labelClassName={styles.label}
+ storageKey={motionStorageKey}
+ value={false}
+ />
+ <AckeeSelect
+ initialValue="full"
+ labelClassName={styles.label}
+ tooltipClassName={tooltipClassName}
+ storageKey={ackeeStorageKey}
+ />
+ </Form>
+ );
+};
+
+export default SettingsForm;
diff --git a/src/components/MDX/Gallery/Gallery.module.scss b/src/components/organisms/images/gallery.module.scss
index 2654b59..a057ed9 100644
--- a/src/components/MDX/Gallery/Gallery.module.scss
+++ b/src/components/organisms/images/gallery.module.scss
@@ -24,9 +24,3 @@
}
}
}
-
-.item {
- > figure {
- margin: 0;
- }
-}
diff --git a/src/components/organisms/images/gallery.stories.tsx b/src/components/organisms/images/gallery.stories.tsx
new file mode 100644
index 0000000..6fc278f
--- /dev/null
+++ b/src/components/organisms/images/gallery.stories.tsx
@@ -0,0 +1,75 @@
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Gallery from './gallery';
+
+/**
+ * Gallery - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Images/Gallery',
+ component: Gallery,
+ argTypes: {
+ children: {
+ control: {
+ type: null,
+ },
+ description: 'Two or more ResponsiveImage component.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ columns: {
+ control: {
+ type: 'number',
+ min: 2,
+ max: 4,
+ },
+ description: 'The columns count.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Gallery>;
+
+const image = {
+ alt: 'Modi provident omnis',
+ height: 480,
+ src: 'http://placeimg.com/640/480/fashion',
+ width: 640,
+};
+
+const Template: ComponentStory<typeof Gallery> = (args) => (
+ <Gallery {...args}>
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ </Gallery>
+);
+
+/**
+ * Gallery Stories - Two columns
+ */
+export const TwoColumns = Template.bind({});
+TwoColumns.args = {
+ columns: 2,
+};
+
+/**
+ * Gallery Stories - Three columns
+ */
+export const ThreeColumns = Template.bind({});
+ThreeColumns.args = {
+ columns: 3,
+};
+
+/**
+ * Gallery Stories - Four columns
+ */
+export const FourColumns = Template.bind({});
+FourColumns.args = {
+ columns: 4,
+};
diff --git a/src/components/organisms/images/gallery.test.tsx b/src/components/organisms/images/gallery.test.tsx
new file mode 100644
index 0000000..5f35f0a
--- /dev/null
+++ b/src/components/organisms/images/gallery.test.tsx
@@ -0,0 +1,38 @@
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import { render, screen } from '@test-utils';
+import Gallery from './gallery';
+
+const columns = 3;
+
+const image = {
+ alt: 'Modi provident omnis',
+ height: 480,
+ src: 'http://placeimg.com/640/480/fashion',
+ width: 640,
+};
+
+describe('Gallery', () => {
+ it('renders the correct number of items', () => {
+ render(
+ <Gallery columns={columns}>
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ </Gallery>
+ );
+ expect(screen.getAllByRole('listitem')).toHaveLength(4);
+ });
+
+ it('renders the right number of columns', () => {
+ render(
+ <Gallery columns={columns}>
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ <ResponsiveImage {...image} />
+ </Gallery>
+ );
+ expect(screen.getByRole('list')).toHaveClass(`wrapper--${columns}-columns`);
+ });
+});
diff --git a/src/components/organisms/images/gallery.tsx b/src/components/organisms/images/gallery.tsx
new file mode 100644
index 0000000..6c4a271
--- /dev/null
+++ b/src/components/organisms/images/gallery.tsx
@@ -0,0 +1,35 @@
+import { type ResponsiveImageProps } from '@components/molecules/images/responsive-image';
+import { Children, FC, ReactElement } from 'react';
+import styles from './gallery.module.scss';
+
+export type GalleryColumn = 2 | 3 | 4;
+
+export type GalleryProps = {
+ /**
+ * The images using ResponsiveImage component.
+ */
+ children: ReactElement<ResponsiveImageProps>[];
+ /**
+ * The columns count.
+ */
+ columns: GalleryColumn;
+};
+
+/**
+ * Gallery component
+ *
+ * Render a gallery of images.
+ */
+const Gallery: FC<GalleryProps> = ({ children, columns }) => {
+ const columnsClass = `wrapper--${columns}-columns`;
+
+ return (
+ <ul className={`${styles.wrapper} ${styles[columnsClass]}`}>
+ {Children.map(children, (child) => {
+ return <li className={styles.item}>{child}</li>;
+ })}
+ </ul>
+ );
+};
+
+export default Gallery;
diff --git a/src/components/organisms/layout/cards-list.module.scss b/src/components/organisms/layout/cards-list.module.scss
new file mode 100644
index 0000000..6274b93
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.module.scss
@@ -0,0 +1,32 @@
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.wrapper {
+ display: grid;
+ grid-template-columns: repeat(
+ auto-fit,
+ min(calc(100vw - (var(--spacing-md) * 2)), var(--card-width, 30ch))
+ );
+ gap: var(--spacing-sm);
+ place-content: center;
+ align-items: stretch;
+ justify-items: stretch;
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "sm") {
+ gap: var(--spacing-lg);
+ }
+ }
+
+ &--ordered {
+ @extend %reset-ordered-list;
+ }
+
+ &--unordered {
+ @extend %reset-list;
+ }
+}
+
+.card {
+ height: 100%;
+}
diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx
new file mode 100644
index 0000000..c19220a
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.stories.tsx
@@ -0,0 +1,136 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CardsListComponent, { type CardsListItem } from './cards-list';
+
+/**
+ * CardsList - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout',
+ component: CardsListComponent,
+ args: {
+ coverFit: 'cover',
+ kind: 'unordered',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ coverFit: {
+ control: {
+ type: 'select',
+ },
+ description: 'The cover fit.',
+ options: ['fill', 'contain', 'cover', 'none', 'scale-down'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'cover' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ description: 'The cards data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list kind.',
+ options: ['ordered', 'unordered'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'unordered' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The heading level for each card.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof CardsListComponent>;
+
+const Template: ComponentStory<typeof CardsListComponent> = (args) => (
+ <CardsListComponent {...args} />
+);
+
+const items: CardsListItem[] = [
+ {
+ id: 'card-1',
+ cover: {
+ alt: 'card 1 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: { thematics: ['Velit', 'Ex', 'Alias'] },
+ tagline: 'Molestias ut error',
+ title: 'Et alias omnis',
+ url: '#',
+ },
+ {
+ id: 'card-2',
+ cover: {
+ alt: 'card 2 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: { thematics: ['Voluptas'] },
+ tagline: 'Quod vel accusamus',
+ title: 'Laboriosam doloremque mollitia',
+ url: '#',
+ },
+ {
+ id: 'card-3',
+ cover: {
+ alt: 'card 3 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: {
+ thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
+ },
+ tagline: 'Quo error eum',
+ title: 'Magni rem nulla',
+ url: '#',
+ },
+];
+
+/**
+ * Layout Stories - Cards list
+ */
+export const CardsList = Template.bind({});
+CardsList.args = {
+ items,
+ titleLevel: 2,
+};
diff --git a/src/components/organisms/layout/cards-list.test.tsx b/src/components/organisms/layout/cards-list.test.tsx
new file mode 100644
index 0000000..8558fa6
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.test.tsx
@@ -0,0 +1,55 @@
+import { render, screen } from '@test-utils';
+import CardsList, { type CardsListItem } from './cards-list';
+
+const items: CardsListItem[] = [
+ {
+ id: 'card-1',
+ cover: {
+ alt: 'card 1 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: { thematics: ['Velit', 'Ex', 'Alias'] },
+ tagline: 'Molestias ut error',
+ title: 'Et alias omnis',
+ url: '#',
+ },
+ {
+ id: 'card-2',
+ cover: {
+ alt: 'card 2 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: { thematics: ['Voluptas'] },
+ tagline: 'Quod vel accusamus',
+ title: 'Laboriosam doloremque mollitia',
+ url: '#',
+ },
+ {
+ id: 'card-3',
+ cover: {
+ alt: 'card 3 picture',
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ height: 480,
+ },
+ meta: {
+ thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
+ },
+ tagline: 'Quo error eum',
+ title: 'Magni rem nulla',
+ url: '#',
+ },
+];
+
+describe('CardsList', () => {
+ it('renders a list of cards', () => {
+ render(<CardsList items={items} titleLevel={2} />);
+ expect(screen.getAllByRole('heading', { level: 2 })).toHaveLength(
+ items.length
+ );
+ });
+});
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
new file mode 100644
index 0000000..1feddd0
--- /dev/null
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -0,0 +1,77 @@
+import List, {
+ type ListItem,
+ type ListProps,
+} from '@components/atoms/lists/list';
+import Card, { type CardProps } from '@components/molecules/layout/card';
+import { FC } from 'react';
+import styles from './cards-list.module.scss';
+
+export type CardsListItem = Omit<
+ CardProps,
+ 'className' | 'coverFit' | 'titleLevel'
+> & {
+ /**
+ * The card id.
+ */
+ id: string;
+};
+
+export type CardsListProps = Pick<CardProps, 'coverFit' | 'titleLevel'> &
+ Pick<ListProps, 'kind'> & {
+ /**
+ * Set additional classnames to the list wrapper.
+ */
+ className?: string;
+ /**
+ * The cards data.
+ */
+ items: CardsListItem[];
+ };
+
+/**
+ * CardsList component
+ *
+ * Return a list of Card components.
+ */
+const CardsList: FC<CardsListProps> = ({
+ className = '',
+ coverFit,
+ items,
+ kind = 'unordered',
+ titleLevel,
+}) => {
+ const kindModifier = `wrapper--${kind}`;
+
+ /**
+ * Format the cards data to be used by the List component.
+ *
+ * @param {CardsListItem[]} cards - An array of card data.
+ * @returns {ListItem[]} The formatted cards data.
+ */
+ const getCards = (cards: CardsListItem[]): ListItem[] => {
+ return cards.map(({ id, ...card }) => {
+ return {
+ id,
+ value: (
+ <Card
+ key={id}
+ coverFit={coverFit}
+ titleLevel={titleLevel}
+ className={styles.card}
+ {...card}
+ />
+ ),
+ };
+ });
+ };
+
+ return (
+ <List
+ kind="flex"
+ items={getCards(items)}
+ className={`${styles.wrapper} ${styles[kindModifier]} ${className}`}
+ />
+ );
+};
+
+export default CardsList;
diff --git a/src/components/organisms/layout/comment.fixture.tsx b/src/components/organisms/layout/comment.fixture.tsx
new file mode 100644
index 0000000..0118139
--- /dev/null
+++ b/src/components/organisms/layout/comment.fixture.tsx
@@ -0,0 +1,41 @@
+import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates';
+import { CommentProps } from './comment';
+
+export const author = {
+ avatar: {
+ alt: 'Author avatar',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ },
+ name: 'Armand',
+ website: 'https://www.armandphilippot.com/',
+};
+
+export const content =
+ 'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.';
+
+export const date = '2021-04-03 23:04:24';
+
+export const meta = {
+ author,
+ date,
+};
+
+export const id = 5;
+
+export const saveComment = async () => {
+ /** Do nothing. */
+};
+
+export const data: CommentProps = {
+ approved: true,
+ content,
+ id,
+ meta,
+ parentId: 0,
+ saveComment,
+};
+
+export const formattedDate = getFormattedDate(date);
+export const formattedTime = getFormattedTime(date);
diff --git a/src/components/organisms/layout/comment.module.scss b/src/components/organisms/layout/comment.module.scss
new file mode 100644
index 0000000..d2b68e1
--- /dev/null
+++ b/src/components/organisms/layout/comment.module.scss
@@ -0,0 +1,91 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ padding: var(--spacing-md);
+ background: var(--color-bg);
+ border: fun.convert-px(1) solid var(--color-border);
+ box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow-light),
+ fun.convert-px(4) fun.convert-px(4) fun.convert-px(3) fun.convert-px(-2)
+ var(--color-shadow);
+
+ &--comment {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ display: grid;
+ grid-template-columns: minmax(0, #{fun.convert-px(150)}) minmax(0, 1fr);
+ column-gap: var(--spacing-lg);
+ }
+ }
+ }
+
+ &--form {
+ display: flex;
+ flex-flow: column wrap;
+ place-content: center;
+ margin-top: var(--spacing-sm);
+ }
+
+ .header {
+ display: flex;
+ flex-flow: column wrap;
+ align-items: center;
+ row-gap: var(--spacing-sm);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ grid-row: 1 / 4;
+ }
+ }
+ }
+
+ .author {
+ color: var(--color-primary-darker);
+ font-weight: 600;
+ text-align: center;
+ }
+
+ .avatar {
+ width: fun.convert-px(85);
+ height: fun.convert-px(85);
+ position: relative;
+ border-radius: fun.convert-px(3);
+ box-shadow: 0 0 0 fun.convert-px(1) var(--color-shadow-light),
+ fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(1)
+ var(--color-shadow);
+
+ img {
+ border-radius: fun.convert-px(3);
+ }
+ }
+
+ .date {
+ margin: var(--spacing-sm) 0;
+ font-size: var(--font-size-sm);
+
+ &__item {
+ justify-content: center;
+ }
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ margin: 0 0 var(--spacing-sm);
+
+ &__item {
+ justify-content: left;
+ }
+ }
+ }
+ }
+
+ .body {
+ overflow-wrap: break-word;
+ }
+
+ .footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding: var(--spacing-md) 0 0;
+ }
+}
diff --git a/src/components/organisms/layout/comment.stories.tsx b/src/components/organisms/layout/comment.stories.tsx
new file mode 100644
index 0000000..7a8ac95
--- /dev/null
+++ b/src/components/organisms/layout/comment.stories.tsx
@@ -0,0 +1,128 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CommentComponent from './comment';
+import { data } from './comment.fixture';
+
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
+/**
+ * Comment - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout/Comment',
+ component: CommentComponent,
+ args: {
+ canReply: true,
+ saveComment,
+ },
+ argTypes: {
+ author: {
+ description: 'The author data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ canReply: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Enable or disable the reply button.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: true },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ content: {
+ control: {
+ type: 'text',
+ },
+ description: 'The comment body.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ id: {
+ control: {
+ type: 'number',
+ },
+ description: 'The comment id.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ Notice: {
+ control: {
+ type: null,
+ },
+ description: 'A component to display a success or error message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ parentId: {
+ control: {
+ type: null,
+ },
+ description: 'The parent id if it is a reply.',
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ publication: {
+ description: 'The publication date.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ saveComment: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to save the comment form data.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof CommentComponent>;
+
+const Template: ComponentStory<typeof CommentComponent> = (args) => (
+ <CommentComponent {...args} />
+);
+
+/**
+ * Layout Stories - Approved
+ */
+export const Approved = Template.bind({});
+Approved.args = {
+ ...data,
+};
+
+/**
+ * Layout Stories - Unapproved
+ */
+export const Unapproved = Template.bind({});
+Unapproved.args = {
+ ...data,
+ approved: false,
+};
diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx
new file mode 100644
index 0000000..66003d1
--- /dev/null
+++ b/src/components/organisms/layout/comment.test.tsx
@@ -0,0 +1,47 @@
+import { render, screen } from '@test-utils';
+import Comment from './comment';
+import {
+ author,
+ data,
+ formattedDate,
+ formattedTime,
+ id,
+} from './comment.fixture';
+
+describe('Comment', () => {
+ it('renders an avatar', () => {
+ render(<Comment canReply={true} {...data} />);
+ expect(
+ screen.getByRole('img', { name: author.avatar.alt })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the author website url', () => {
+ render(<Comment canReply={true} {...data} />);
+ expect(screen.getByRole('link', { name: author.name })).toHaveAttribute(
+ 'href',
+ author.website
+ );
+ });
+
+ it('renders a permalink to the comment', () => {
+ render(<Comment canReply={true} {...data} />);
+ expect(
+ screen.getByRole('link', {
+ name: `${formattedDate} at ${formattedTime}`,
+ })
+ ).toHaveAttribute('href', `/#comment-${id}`);
+ });
+
+ it('renders a reply button', () => {
+ render(<Comment canReply={true} {...data} />);
+ expect(screen.getByRole('button', { name: 'Reply' })).toBeInTheDocument();
+ });
+
+ it('does not render a reply button', () => {
+ render(<Comment canReply={false} {...data} />);
+ expect(
+ screen.queryByRole('button', { name: 'Reply' })
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx
new file mode 100644
index 0000000..f62f95c
--- /dev/null
+++ b/src/components/organisms/layout/comment.tsx
@@ -0,0 +1,171 @@
+import Button from '@components/atoms/buttons/button';
+import Link from '@components/atoms/links/link';
+import Meta from '@components/molecules/layout/meta';
+import { type Comment as CommentType } from '@ts/types/app';
+import useSettings from '@utils/hooks/use-settings';
+import Image from 'next/image';
+import Script from 'next/script';
+import { FC, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { type Comment as CommentSchema, type WithContext } from 'schema-dts';
+import CommentForm, { type CommentFormProps } from '../forms/comment-form';
+import styles from './comment.module.scss';
+
+export type CommentProps = Pick<
+ CommentType,
+ 'approved' | 'content' | 'id' | 'meta' | 'parentId'
+> &
+ Pick<CommentFormProps, 'Notice' | 'saveComment'> & {
+ /**
+ * Enable or disable the reply button. Default: true.
+ */
+ canReply?: boolean;
+ };
+
+/**
+ * Comment component
+ *
+ * Render a single comment.
+ */
+const Comment: FC<CommentProps> = ({
+ approved,
+ canReply = true,
+ content,
+ id,
+ meta,
+ Notice,
+ parentId,
+ saveComment,
+ ...props
+}) => {
+ const intl = useIntl();
+ const { website } = useSettings();
+ const [isReplying, setIsReplying] = useState<boolean>(false);
+
+ if (!approved) {
+ return (
+ <div className={styles.wrapper}>
+ {intl.formatMessage({
+ defaultMessage: 'This comment is awaiting moderation...',
+ description: 'Comment: awaiting moderation',
+ id: '6a1Uo6',
+ })}
+ </div>
+ );
+ }
+
+ const { author, date } = meta;
+ const [publicationDate, publicationTime] = date.split(' ');
+
+ const buttonLabel = isReplying
+ ? intl.formatMessage({
+ defaultMessage: 'Cancel reply',
+ description: 'Comment: cancel reply button',
+ id: 'LCorTC',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'Reply',
+ description: 'Comment: reply button',
+ id: 'hzHuCc',
+ });
+ const formTitle = intl.formatMessage({
+ defaultMessage: 'Leave a reply',
+ description: 'Comment: comment form title',
+ id: '2fD5CI',
+ });
+
+ const commentSchema: WithContext<CommentSchema> = {
+ '@context': 'https://schema.org',
+ '@id': `${website.url}/#comment-${id}`,
+ '@type': 'Comment',
+ parentItem: parentId
+ ? { '@id': `${website.url}/#comment-${parentId}` }
+ : undefined,
+ about: { '@type': 'Article', '@id': `${website.url}/#article` },
+ author: {
+ '@type': 'Person',
+ name: author.name,
+ image: author.avatar?.src,
+ url: author.website,
+ },
+ creator: {
+ '@type': 'Person',
+ name: author.name,
+ image: author.avatar?.src,
+ url: author.website,
+ },
+ dateCreated: date,
+ datePublished: date,
+ text: content,
+ };
+
+ return (
+ <>
+ <Script
+ id="schema-comments"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(commentSchema) }}
+ />
+ <article
+ id={`comment-${id}`}
+ className={`${styles.wrapper} ${styles['wrapper--comment']}`}
+ >
+ <header className={styles.header}>
+ {author.avatar && (
+ <div className={styles.avatar}>
+ <Image
+ src={author.avatar.src}
+ alt={author.avatar.alt}
+ layout="fill"
+ objectFit="cover"
+ {...props}
+ />
+ </div>
+ )}
+ {author.website ? (
+ <Link href={author.website} className={styles.author}>
+ {author.name}
+ </Link>
+ ) : (
+ <span className={styles.author}>{author.name}</span>
+ )}
+ </header>
+ <Meta
+ data={{
+ publication: {
+ date: publicationDate,
+ time: publicationTime,
+ target: `#comment-${id}`,
+ },
+ }}
+ layout="inline"
+ itemsLayout="inline"
+ className={styles.date}
+ groupClassName={styles.date__item}
+ />
+ <div
+ className={styles.body}
+ dangerouslySetInnerHTML={{ __html: content }}
+ />
+ <footer className={styles.footer}>
+ {canReply && (
+ <Button kind="tertiary" onClick={() => setIsReplying(!isReplying)}>
+ {buttonLabel}
+ </Button>
+ )}
+ </footer>
+ </article>
+ {isReplying && (
+ <CommentForm
+ Notice={Notice}
+ parentId={id}
+ saveComment={saveComment}
+ title={formTitle}
+ className={`${styles.wrapper} ${styles['wrapper--form']}`}
+ />
+ )}
+ </>
+ );
+};
+
+export default Comment;
diff --git a/src/components/organisms/layout/comments-list.fixture.tsx b/src/components/organisms/layout/comments-list.fixture.tsx
new file mode 100644
index 0000000..2618f77
--- /dev/null
+++ b/src/components/organisms/layout/comments-list.fixture.tsx
@@ -0,0 +1,106 @@
+import { Comment } from '@ts/types/app';
+
+export const comments: Comment[] = [
+ {
+ approved: true,
+ content:
+ 'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.',
+ id: 1,
+ meta: {
+ author: {
+ avatar: {
+ alt: 'Author 1 avatar',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ },
+ name: 'Author 1',
+ },
+ date: '2021-04-03 18:04:11',
+ },
+ parentId: 0,
+ replies: [],
+ },
+ {
+ approved: true,
+ content:
+ 'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam occaecati.',
+ id: 2,
+ meta: {
+ author: {
+ avatar: {
+ alt: 'Author 2 avatar',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ },
+ name: 'Author 2',
+ website: '#',
+ },
+ date: '2021-04-03 23:30:20',
+ },
+ parentId: 0,
+ replies: [
+ {
+ approved: true,
+ content:
+ 'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.',
+ id: 4,
+ meta: {
+ author: {
+ avatar: {
+ alt: 'Author 4 avatar',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ },
+ name: 'Author 4',
+ },
+ date: '2021-04-03 23:04:24',
+ },
+ parentId: 2,
+ replies: [],
+ },
+ {
+ approved: true,
+ content:
+ 'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.',
+ id: 5,
+ meta: {
+ author: {
+ avatar: {
+ alt: 'Author 1 avatar',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ },
+ name: 'Author 1',
+ },
+ date: '2021-04-04 08:05:14',
+ },
+ parentId: 2,
+ replies: [],
+ },
+ ],
+ },
+ {
+ approved: false,
+ content:
+ 'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.',
+ id: 3,
+ meta: {
+ author: {
+ avatar: {
+ alt: 'Author 3',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+ },
+ name: 'Author 3',
+ },
+ date: '2021-09-13 13:24:54',
+ },
+ parentId: 0,
+ replies: [],
+ },
+];
diff --git a/src/components/organisms/layout/comments-list.module.scss b/src/components/organisms/layout/comments-list.module.scss
new file mode 100644
index 0000000..803a418
--- /dev/null
+++ b/src/components/organisms/layout/comments-list.module.scss
@@ -0,0 +1,16 @@
+@use "@styles/abstracts/placeholders";
+
+.list {
+ @extend %reset-ordered-list;
+
+ & & {
+ margin: var(--spacing-sm) 0;
+ padding-left: var(--spacing-sm);
+ }
+}
+
+.item {
+ &:not(:last-child) {
+ margin-bottom: var(--spacing-sm);
+ }
+}
diff --git a/src/components/organisms/layout/comments-list.stories.tsx b/src/components/organisms/layout/comments-list.stories.tsx
new file mode 100644
index 0000000..5ed0f2a
--- /dev/null
+++ b/src/components/organisms/layout/comments-list.stories.tsx
@@ -0,0 +1,91 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CommentsListComponent from './comments-list';
+import { comments } from './comments-list.fixture';
+
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
+/**
+ * CommentsList - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout/CommentsList',
+ component: CommentsListComponent,
+ args: {
+ saveComment,
+ },
+ argTypes: {
+ comments: {
+ control: {
+ type: null,
+ },
+ description: 'An array of comments.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ depth: {
+ control: {
+ type: 'number',
+ min: 0,
+ max: 4,
+ },
+ description: 'The maximum depth. Use `0` to not display nested comments.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ Notice: {
+ control: {
+ type: null,
+ },
+ description: 'A component to display a success or error message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ saveComment: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to save the comment form data.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof CommentsListComponent>;
+
+const Template: ComponentStory<typeof CommentsListComponent> = (args) => (
+ <CommentsListComponent {...args} />
+);
+
+/**
+ * Layout Stories - Without child comments
+ */
+export const WithoutChildComments = Template.bind({});
+WithoutChildComments.args = {
+ comments,
+ depth: 0,
+};
+
+/**
+ * Layout Stories - With child comments
+ */
+export const WithChildComments = Template.bind({});
+WithChildComments.args = {
+ comments,
+ depth: 1,
+};
diff --git a/src/components/organisms/layout/comments-list.test.tsx b/src/components/organisms/layout/comments-list.test.tsx
new file mode 100644
index 0000000..b0a2467
--- /dev/null
+++ b/src/components/organisms/layout/comments-list.test.tsx
@@ -0,0 +1,12 @@
+import { render } from '@test-utils';
+import { saveComment } from './comment.fixture';
+import CommentsList from './comments-list';
+import { comments } from './comments-list.fixture';
+
+describe('CommentsList', () => {
+ it('renders a comments list', () => {
+ render(
+ <CommentsList comments={comments} depth={1} saveComment={saveComment} />
+ );
+ });
+});
diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx
new file mode 100644
index 0000000..97eccb7
--- /dev/null
+++ b/src/components/organisms/layout/comments-list.tsx
@@ -0,0 +1,60 @@
+import SingleComment, {
+ type CommentProps,
+} from '@components/organisms/layout/comment';
+import { Comment } from '@ts/types/app';
+import { FC } from 'react';
+import styles from './comments-list.module.scss';
+
+export type CommentsListProps = Pick<CommentProps, 'Notice' | 'saveComment'> & {
+ /**
+ * An array of comments.
+ */
+ comments: Comment[];
+ /**
+ * The maximum depth. Use `0` to not display nested comments.
+ */
+ depth: 0 | 1 | 2 | 3 | 4;
+};
+
+/**
+ * CommentsList component
+ *
+ * Render a comments list.
+ */
+const CommentsList: FC<CommentsListProps> = ({
+ comments,
+ depth,
+ Notice,
+ saveComment,
+}) => {
+ /**
+ * Get each comment wrapped in a list item.
+ *
+ * @param {Comment[]} commentsList - An array of comments.
+ * @returns {JSX.Element[]} The list items.
+ */
+ const getItems = (
+ commentsList: Comment[],
+ startLevel: number
+ ): JSX.Element[] => {
+ const isLastLevel = startLevel === depth;
+
+ return commentsList.map(({ replies, ...comment }) => (
+ <li key={comment.id} className={styles.item}>
+ <SingleComment
+ canReply={!isLastLevel}
+ Notice={Notice}
+ saveComment={saveComment}
+ {...comment}
+ />
+ {replies && !isLastLevel && (
+ <ol className={styles.list}>{getItems(replies, startLevel + 1)}</ol>
+ )}
+ </li>
+ ));
+ };
+
+ return <ol className={styles.list}>{getItems(comments, 0)}</ol>;
+};
+
+export default CommentsList;
diff --git a/src/components/organisms/layout/footer.module.scss b/src/components/organisms/layout/footer.module.scss
new file mode 100644
index 0000000..c180e86
--- /dev/null
+++ b/src/components/organisms/layout/footer.module.scss
@@ -0,0 +1,41 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: flex;
+ flex-flow: column wrap;
+ gap: var(--spacing-xs);
+ place-items: center;
+ place-content: center;
+ padding: var(--spacing-md) 0 calc(var(--toolbar-size) + var(--spacing-md));
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ --toolbar-size: 0px;
+
+ flex-flow: row wrap;
+ font-size: var(--font-size-sm);
+ }
+ }
+}
+
+.nav {
+ display: flex;
+ flex-flow: row wrap;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ &::before {
+ content: "\2022";
+ margin-right: var(--spacing-2xs);
+ }
+ }
+ }
+}
+
+.back-to-top {
+ position: fixed;
+ bottom: calc(var(--toolbar-size, 0px) + var(--spacing-md));
+ right: var(--spacing-md);
+ transition: all 0.4s ease-in 0s;
+}
diff --git a/src/components/organisms/layout/footer.stories.tsx b/src/components/organisms/layout/footer.stories.tsx
new file mode 100644
index 0000000..bd5a744
--- /dev/null
+++ b/src/components/organisms/layout/footer.stories.tsx
@@ -0,0 +1,90 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import FooterComponent from './footer';
+
+/**
+ * Footer - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout',
+ component: FooterComponent,
+ argTypes: {
+ backToTopClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the back to top button.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the footer element.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ copyright: {
+ description: 'The copyright information.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ navItems: {
+ description: 'The footer nav items.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ topId: {
+ control: {
+ type: 'text',
+ },
+ description:
+ 'An element id (without hashtag) used as target by back to top button.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof FooterComponent>;
+
+const Template: ComponentStory<typeof FooterComponent> = (args) => (
+ <FooterComponent {...args} />
+);
+
+const copyright = {
+ dates: { start: '2017', end: '2022' },
+ owner: 'Lorem ipsum',
+ icon: 'CC',
+};
+
+const navItems = [{ id: 'legal-notice', href: '#', label: 'Legal notice' }];
+
+/**
+ * Layout Stories - Footer
+ */
+export const Footer = Template.bind({});
+Footer.args = {
+ copyright,
+ navItems,
+ topId: 'top',
+};
diff --git a/src/components/organisms/layout/footer.test.tsx b/src/components/organisms/layout/footer.test.tsx
new file mode 100644
index 0000000..bc23732
--- /dev/null
+++ b/src/components/organisms/layout/footer.test.tsx
@@ -0,0 +1,33 @@
+import { render, screen } from '@test-utils';
+import Footer, { type FooterProps } from './footer';
+
+const copyright: FooterProps['copyright'] = {
+ dates: { start: '2017', end: '2022' },
+ owner: 'Lorem ipsum',
+ icon: 'CC',
+};
+
+const navItems: FooterProps['navItems'] = [
+ { id: 'legal-notice', href: '#', label: 'Legal notice' },
+];
+
+describe('Footer', () => {
+ it('renders the website copyright', () => {
+ render(<Footer copyright={copyright} topId="top" />);
+ expect(screen.getByText(copyright.owner)).toBeInTheDocument();
+ });
+
+ it('renders a back to top link', () => {
+ render(<Footer copyright={copyright} topId="top" />);
+ expect(
+ screen.getByRole('link', { name: 'Back to top' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders some nav items', () => {
+ render(<Footer copyright={copyright} navItems={navItems} topId="top" />);
+ expect(
+ screen.getByRole('link', { name: navItems[0].label })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/footer.tsx b/src/components/organisms/layout/footer.tsx
new file mode 100644
index 0000000..c60afec
--- /dev/null
+++ b/src/components/organisms/layout/footer.tsx
@@ -0,0 +1,77 @@
+import Copyright, {
+ type CopyrightProps,
+} from '@components/atoms/layout/copyright';
+import BackToTop, {
+ type BackToTopProps,
+} from '@components/molecules/buttons/back-to-top';
+import Nav, { type NavItem } from '@components/molecules/nav/nav';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './footer.module.scss';
+
+export type FooterProps = {
+ /**
+ * Set additional classnames to the back to top button.
+ */
+ backToTopClassName?: BackToTopProps['className'];
+ /**
+ * Set additional classnames to the footer element.
+ */
+ className?: string;
+ /**
+ * Set the copyright information.
+ */
+ copyright: CopyrightProps;
+ /**
+ * The footer nav items.
+ */
+ navItems?: NavItem[];
+ /**
+ * An element id (without hashtag) used as anchor for back to top button.
+ */
+ topId: string;
+};
+
+/**
+ * Footer component
+ *
+ * Renders a footer with copyright and nav;
+ */
+const Footer: FC<FooterProps> = ({
+ backToTopClassName,
+ className = '',
+ copyright,
+ navItems,
+ topId,
+}) => {
+ const intl = useIntl();
+ const ariaLabel = intl.formatMessage({
+ defaultMessage: 'Footer',
+ description: 'Footer: an accessible name for footer nav',
+ id: 'd4N8nD',
+ });
+
+ return (
+ <footer className={`${styles.wrapper} ${className}`}>
+ <Copyright
+ dates={copyright.dates}
+ owner={copyright.owner}
+ icon={copyright.icon}
+ />
+ {navItems && (
+ <Nav
+ aria-label={ariaLabel}
+ kind="footer"
+ items={navItems}
+ className={styles.nav}
+ />
+ )}
+ <BackToTop
+ target={topId}
+ className={`${styles['back-to-top']} ${backToTopClassName}`}
+ />
+ </footer>
+ );
+};
+
+export default Footer;
diff --git a/src/components/organisms/layout/header.module.scss b/src/components/organisms/layout/header.module.scss
new file mode 100644
index 0000000..a98cf45
--- /dev/null
+++ b/src/components/organisms/layout/header.module.scss
@@ -0,0 +1,50 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: grid;
+ grid-template-columns:
+ minmax(0, 1fr) min(calc(100vw - calc(var(--spacing-md) * 2)), 100ch)
+ minmax(0, 1fr);
+ align-items: center;
+ padding: var(--spacing-md) 0 var(--spacing-lg);
+
+ .toolbar {
+ justify-content: space-around;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: 5;
+ background: var(--color-bg);
+ border-top: fun.convert-px(4) solid;
+ border-image: radial-gradient(
+ ellipse at top,
+ var(--color-primary-lighter) 20%,
+ var(--color-primary) 100%
+ )
+ 1;
+ box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1)
+ var(--color-shadow-dark);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ justify-content: flex-end;
+ width: auto;
+ position: relative;
+ left: unset;
+ background: inherit;
+ border: none;
+ box-shadow: none;
+ }
+ }
+ }
+}
+
+.body {
+ grid-column: 2;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--spacing-md);
+}
diff --git a/src/components/organisms/layout/header.stories.tsx b/src/components/organisms/layout/header.stories.tsx
new file mode 100644
index 0000000..0507e89
--- /dev/null
+++ b/src/components/organisms/layout/header.stories.tsx
@@ -0,0 +1,153 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import HeaderComponent from './header';
+
+/**
+ * Header - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout',
+ component: HeaderComponent,
+ args: {
+ ackeeStorageKey: 'ackee-tracking',
+ isHome: false,
+ motionStorageKey: 'reduced-motion',
+ searchPage: '#',
+ withLink: false,
+ },
+ argTypes: {
+ ackeeStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set Ackee settings local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ baseline: {
+ control: {
+ type: 'text',
+ },
+ description: 'The branding baseline.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the header wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isHome: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the current page is homepage or not.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ motionStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set Reduced motion settings local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ nav: {
+ description: 'The main navigation items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ photo: {
+ control: {
+ type: 'text',
+ },
+ description: 'The branding photo.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ searchPage: {
+ control: {
+ type: 'text',
+ },
+ description: 'The search results page url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The website title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ withLink: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Wrap the website title with a link to homepage.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof HeaderComponent>;
+
+const Template: ComponentStory<typeof HeaderComponent> = (args) => (
+ <HeaderComponent {...args} />
+);
+
+const nav = [
+ { id: 'home-link', href: '#', label: 'Home' },
+ { id: 'blog-link', href: '#', label: 'Blog' },
+ { id: 'cv-link', href: '#', label: 'CV' },
+ { id: 'contact-link', href: '#', label: 'Contact' },
+];
+
+/**
+ * Layout Stories - Header
+ */
+export const Header = Template.bind({});
+Header.args = {
+ nav,
+ photo: 'http://placeimg.com/640/480/people',
+ title: 'Website title',
+};
diff --git a/src/components/organisms/layout/header.test.tsx b/src/components/organisms/layout/header.test.tsx
new file mode 100644
index 0000000..414d96f
--- /dev/null
+++ b/src/components/organisms/layout/header.test.tsx
@@ -0,0 +1,46 @@
+import { render, screen } from '@test-utils';
+import Header from './header';
+
+const nav = [
+ { id: 'home-link', href: '#', label: 'Home' },
+ { id: 'blog-link', href: '#', label: 'Blog' },
+ { id: 'cv-link', href: '#', label: 'CV' },
+ { id: 'contact-link', href: '#', label: 'Contact' },
+];
+
+const photo = 'http://placeimg.com/640/480/nightlife';
+
+const title = 'Assumenda quis quod';
+
+describe('Header', () => {
+ it('renders the website title', () => {
+ render(
+ <Header
+ ackeeStorageKey="ackee-tracking"
+ isHome={true}
+ motionStorageKey="reduced-motion"
+ nav={nav}
+ photo={photo}
+ searchPage="#"
+ title={title}
+ />
+ );
+ expect(
+ screen.getByRole('heading', { level: 1, name: title })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the main nav', () => {
+ render(
+ <Header
+ ackeeStorageKey="ackee-tracking"
+ motionStorageKey="reduced-motion"
+ nav={nav}
+ photo={photo}
+ searchPage="#"
+ title={title}
+ />
+ );
+ expect(screen.getByRole('navigation')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/header.tsx b/src/components/organisms/layout/header.tsx
new file mode 100644
index 0000000..f6212c3
--- /dev/null
+++ b/src/components/organisms/layout/header.tsx
@@ -0,0 +1,48 @@
+import Branding, {
+ type BrandingProps,
+} from '@components/molecules/layout/branding';
+import { FC } from 'react';
+import Toolbar, { type ToolbarProps } from '../toolbar/toolbar';
+import styles from './header.module.scss';
+
+export type HeaderProps = BrandingProps &
+ Pick<
+ ToolbarProps,
+ 'ackeeStorageKey' | 'motionStorageKey' | 'nav' | 'searchPage'
+ > & {
+ /**
+ * Set additional classnames to the header element.
+ */
+ className?: string;
+ };
+
+/**
+ * Header component
+ *
+ * Render the website header.
+ */
+const Header: FC<HeaderProps> = ({
+ ackeeStorageKey,
+ className,
+ motionStorageKey,
+ nav,
+ searchPage,
+ ...props
+}) => {
+ return (
+ <header className={`${styles.wrapper} ${className}`}>
+ <div className={styles.body}>
+ <Branding {...props} />
+ <Toolbar
+ ackeeStorageKey={ackeeStorageKey}
+ className={styles.toolbar}
+ motionStorageKey={motionStorageKey}
+ nav={nav}
+ searchPage={searchPage}
+ />
+ </div>
+ </header>
+ );
+};
+
+export default Header;
diff --git a/src/components/organisms/layout/no-results.stories.tsx b/src/components/organisms/layout/no-results.stories.tsx
new file mode 100644
index 0000000..aa2e51e
--- /dev/null
+++ b/src/components/organisms/layout/no-results.stories.tsx
@@ -0,0 +1,28 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import NoResultsComponent from './no-results';
+
+export default {
+ title: 'Organisms/Layout',
+ component: NoResultsComponent,
+ argTypes: {
+ searchPage: {
+ control: {
+ type: 'text',
+ },
+ description: 'The search results page.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof NoResultsComponent>;
+
+const Template: ComponentStory<typeof NoResultsComponent> = (args) => (
+ <NoResultsComponent {...args} />
+);
+
+export const NoResults = Template.bind({});
+NoResults.args = {
+ searchPage: '#',
+};
diff --git a/src/components/organisms/layout/no-results.test.tsx b/src/components/organisms/layout/no-results.test.tsx
new file mode 100644
index 0000000..7f57177
--- /dev/null
+++ b/src/components/organisms/layout/no-results.test.tsx
@@ -0,0 +1,14 @@
+import { render, screen } from '@test-utils';
+import NoResults from './no-results';
+
+describe('NoResults', () => {
+ it('renders a no results text', () => {
+ render(<NoResults searchPage="#" />);
+ expect(screen.getByText(/No results/gi)).toBeInTheDocument();
+ });
+
+ it('renders a search form', () => {
+ render(<NoResults searchPage="#" />);
+ expect(screen.getByRole('searchbox')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/no-results.tsx b/src/components/organisms/layout/no-results.tsx
new file mode 100644
index 0000000..2245dbf
--- /dev/null
+++ b/src/components/organisms/layout/no-results.tsx
@@ -0,0 +1,38 @@
+import SearchForm, {
+ type SearchFormProps,
+} from '@components/organisms/forms/search-form';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+
+export type NoResultsProps = Pick<SearchFormProps, 'searchPage'>;
+
+/**
+ * NoResults component
+ *
+ * Renders a no results text with a search form.
+ */
+const NoResults: FC<NoResultsProps> = ({ searchPage }) => {
+ const intl = useIntl();
+
+ return (
+ <>
+ <p>
+ {intl.formatMessage({
+ defaultMessage: 'No results found.',
+ description: 'NoResults: no results',
+ id: '5O2vpy',
+ })}
+ </p>
+ <p>
+ {intl.formatMessage({
+ defaultMessage: 'Would you like to try a new search?',
+ description: 'NoResults: try a new search message',
+ id: 'DVBwfu',
+ })}
+ </p>
+ <SearchForm hideLabel={true} searchPage={searchPage} />
+ </>
+ );
+};
+
+export default NoResults;
diff --git a/src/components/organisms/layout/overview.module.scss b/src/components/organisms/layout/overview.module.scss
new file mode 100644
index 0000000..895bae5
--- /dev/null
+++ b/src/components/organisms/layout/overview.module.scss
@@ -0,0 +1,44 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ padding: var(--spacing-sm) var(--spacing-md);
+ border: fun.convert-px(1) solid var(--color-border);
+
+ .meta {
+ display: grid;
+ grid-template-columns: repeat(
+ auto-fit,
+ min(calc(100vw - (var(--spacing-md) * 2)), 23ch)
+ );
+ row-gap: var(--spacing-2xs);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ grid-template-columns: repeat(
+ auto-fit,
+ min(calc(100vw - (var(--spacing-md) * 2)), 20ch)
+ );
+ }
+ }
+
+ &--has-techno {
+ div:last-child {
+ gap: var(--spacing-2xs);
+
+ dd {
+ padding: 0 var(--spacing-2xs);
+ border: fun.convert-px(1) solid var(--color-border-dark);
+ }
+ }
+ }
+ }
+
+ .cover {
+ width: fit-content;
+ max-height: fun.convert-px(175);
+ margin-bottom: var(--spacing-sm);
+ padding: var(--spacing-2xs);
+ border: fun.convert-px(1) solid var(--color-border);
+ }
+}
diff --git a/src/components/organisms/layout/overview.stories.tsx b/src/components/organisms/layout/overview.stories.tsx
new file mode 100644
index 0000000..26f7ba0
--- /dev/null
+++ b/src/components/organisms/layout/overview.stories.tsx
@@ -0,0 +1,77 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Overview, { OverviewMeta } from './overview';
+
+/**
+ * Overview - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout/Overview',
+ component: Overview,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the overview wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ cover: {
+ description: 'The overview cover',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ meta: {
+ description: 'The overview meta.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ },
+} as ComponentMeta<typeof Overview>;
+
+const Template: ComponentStory<typeof Overview> = (args) => (
+ <Overview {...args} />
+);
+
+const cover = {
+ alt: 'picture',
+ height: 480,
+ src: 'http://placeimg.com/640/480/cats',
+ width: 640,
+};
+
+const meta: OverviewMeta = {
+ creation: { date: '2022-05-09' },
+ license: 'Dignissimos ratione veritatis',
+};
+
+/**
+ * Overview Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ meta,
+};
+
+/**
+ * Overview Stories - With cover
+ */
+export const WithCover = Template.bind({});
+WithCover.args = {
+ cover,
+ meta,
+};
diff --git a/src/components/organisms/layout/overview.test.tsx b/src/components/organisms/layout/overview.test.tsx
new file mode 100644
index 0000000..b40a785
--- /dev/null
+++ b/src/components/organisms/layout/overview.test.tsx
@@ -0,0 +1,26 @@
+import { render, screen } from '@test-utils';
+import Overview, { type OverviewMeta } from './overview';
+
+const cover = {
+ alt: 'Incidunt unde quam',
+ height: 480,
+ src: 'http://placeimg.com/640/480/cats',
+ width: 640,
+};
+
+const data: OverviewMeta = {
+ creation: { date: '2022-05-09' },
+ license: 'Dignissimos ratione veritatis',
+};
+
+describe('Overview', () => {
+ it('renders some data', () => {
+ render(<Overview meta={data} />);
+ expect(screen.getByText(data.license!)).toBeInTheDocument();
+ });
+
+ it('renders a cover', () => {
+ render(<Overview cover={cover} meta={data} />);
+ expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
new file mode 100644
index 0000000..b110e68
--- /dev/null
+++ b/src/components/organisms/layout/overview.tsx
@@ -0,0 +1,61 @@
+import ResponsiveImage, {
+ type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
+import Meta, { type MetaData } from '@components/molecules/layout/meta';
+import { FC } from 'react';
+import styles from './overview.module.scss';
+
+export type OverviewMeta = Pick<
+ MetaData,
+ | 'creation'
+ | 'license'
+ | 'popularity'
+ | 'repositories'
+ | 'technologies'
+ | 'update'
+>;
+
+export type OverviewProps = {
+ /**
+ * Set additional classnames to the overview wrapper.
+ */
+ className?: string;
+ /**
+ * The overview cover.
+ */
+ cover?: Pick<ResponsiveImageProps, 'alt' | 'src' | 'width' | 'height'>;
+ /**
+ * The overview meta.
+ */
+ meta: OverviewMeta;
+};
+
+/**
+ * Overview component
+ *
+ * Render an overview.
+ */
+const Overview: FC<OverviewProps> = ({ className = '', cover, meta }) => {
+ const { technologies, ...remainingMeta } = meta;
+ const metaModifier = technologies ? styles['meta--has-techno'] : '';
+
+ return (
+ <div className={`${styles.wrapper} ${className}`}>
+ {cover && (
+ <ResponsiveImage
+ className={styles.cover}
+ objectFit="contain"
+ {...cover}
+ />
+ )}
+ <Meta
+ data={{ ...remainingMeta, technologies }}
+ layout="inline"
+ className={`${styles.meta} ${metaModifier}`}
+ withSeparator={false}
+ />
+ </div>
+ );
+};
+
+export default Overview;
diff --git a/src/components/organisms/layout/posts-list.fixture.tsx b/src/components/organisms/layout/posts-list.fixture.tsx
new file mode 100644
index 0000000..97a746f
--- /dev/null
+++ b/src/components/organisms/layout/posts-list.fixture.tsx
@@ -0,0 +1,63 @@
+import { type Post } from './posts-list';
+
+export const introPost1 =
+ 'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.';
+
+export const introPost2 =
+ 'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.';
+
+export const introPost3 =
+ 'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.';
+
+export const cover = {
+ alt: 'cover',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+};
+
+export const posts: Post[] = [
+ {
+ intro: introPost1,
+ id: 'post-1',
+ meta: {
+ cover,
+ dates: { publication: '2022-02-26' },
+ wordsCount: introPost1.split(' ').length,
+ thematics: [
+ { id: 1, name: 'Cat 1', url: '#' },
+ { id: 2, name: 'Cat 2', url: '#' },
+ ],
+ commentsCount: 1,
+ },
+ title: 'Ratione velit fuga',
+ url: '#',
+ },
+ {
+ intro: introPost2,
+ id: 'post-2',
+ meta: {
+ dates: { publication: '2022-02-20' },
+ wordsCount: introPost2.split(' ').length,
+ thematics: [{ id: 2, name: 'Cat 2', url: '#' }],
+ commentsCount: 0,
+ },
+ title: 'Debitis laudantium laudantium',
+ url: '#',
+ },
+ {
+ intro: introPost3,
+ id: 'post-3',
+ meta: {
+ cover,
+ dates: { publication: '2021-12-20' },
+ wordsCount: introPost3.split(' ').length,
+ thematics: [{ id: 1, name: 'Cat 1', url: '#' }],
+ commentsCount: 3,
+ },
+ title: 'Quaerat ut corporis',
+ url: '#',
+ },
+];
+
+export const searchPage = '#';
diff --git a/src/components/organisms/layout/posts-list.module.scss b/src/components/organisms/layout/posts-list.module.scss
new file mode 100644
index 0000000..b09bb12
--- /dev/null
+++ b/src/components/organisms/layout/posts-list.module.scss
@@ -0,0 +1,62 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.section {
+ &:not(:last-of-type) {
+ margin-bottom: var(--spacing-md);
+ }
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ display: grid;
+ grid-template-columns: fun.convert-px(150) minmax(0, 1fr);
+ align-items: first baseline;
+ margin-left: fun.convert-px(-150);
+ }
+ }
+}
+
+.list {
+ @extend %reset-ordered-list;
+
+ .item {
+ border-bottom: fun.convert-px(1) solid var(--color-border);
+
+ &:not(:last-child) {
+ margin-bottom: var(--spacing-md);
+ }
+ }
+}
+
+.year {
+ padding-bottom: fun.convert-px(3);
+ background: linear-gradient(
+ to top,
+ var(--color-primary-dark) 0.3rem,
+ transparent 0.3rem
+ )
+ 0 0 / 3rem 100% no-repeat;
+ font-size: var(--font-size-2xl);
+ font-weight: 500;
+ text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ grid-column: 1;
+ justify-self: end;
+ padding-right: var(--spacing-lg);
+ position: sticky;
+ top: var(--spacing-xs);
+ }
+
+ @include mix.dimensions("lg") {
+ padding-right: var(--spacing-xl);
+ }
+ }
+}
+
+.btn {
+ display: flex;
+ margin: auto;
+}
diff --git a/src/components/organisms/layout/posts-list.stories.tsx b/src/components/organisms/layout/posts-list.stories.tsx
new file mode 100644
index 0000000..bff1f28
--- /dev/null
+++ b/src/components/organisms/layout/posts-list.stories.tsx
@@ -0,0 +1,194 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import PostsList from './posts-list';
+import { posts, searchPage } from './posts-list.fixture';
+
+/**
+ * PostsList - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout/PostsList',
+ component: PostsList,
+ args: {
+ byYear: false,
+ isLoading: false,
+ pageNumber: 1,
+ showLoadMoreBtn: false,
+ siblings: 1,
+ titleLevel: 2,
+ },
+ argTypes: {
+ baseUrl: {
+ control: {
+ type: 'text',
+ },
+ description: 'The pagination base url.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: '/page/' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ byYear: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'True to display the posts by year.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ isLoading: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the data is loading.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ loadMore: {
+ control: {
+ type: null,
+ },
+ description: 'A function to load more posts on button click.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ pageNumber: {
+ control: {
+ type: 'number',
+ },
+ description: 'The current page number.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 1 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ posts: {
+ description: 'The posts data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ showLoadMoreBtn: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the load more button should be visible.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ siblings: {
+ control: {
+ type: 'number',
+ },
+ description: 'The number of page siblings inside pagination.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 1 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The title level (hn).',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 2 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ total: {
+ control: {
+ type: 'number',
+ },
+ description: 'The number of posts.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof PostsList>;
+
+const Template: ComponentStory<typeof PostsList> = (args) => (
+ <PostsList {...args} />
+);
+
+/**
+ * PostsList Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ posts,
+ searchPage,
+ total: posts.length,
+};
+
+/**
+ * PostsList Stories - By years
+ */
+export const ByYears = Template.bind({});
+ByYears.args = {
+ posts,
+ byYear: true,
+ searchPage,
+ total: posts.length,
+};
+ByYears.decorators = [
+ (Story) => (
+ <div style={{ marginLeft: 150 }}>
+ <Story />
+ </div>
+ ),
+];
+
+/**
+ * PostsList Stories - No results
+ */
+export const NoResults = Template.bind({});
+NoResults.args = {
+ posts: [],
+ searchPage,
+ total: posts.length,
+};
diff --git a/src/components/organisms/layout/posts-list.test.tsx b/src/components/organisms/layout/posts-list.test.tsx
new file mode 100644
index 0000000..e58a974
--- /dev/null
+++ b/src/components/organisms/layout/posts-list.test.tsx
@@ -0,0 +1,46 @@
+import { render, screen } from '@test-utils';
+import PostsList from './posts-list';
+import { posts, searchPage } from './posts-list.fixture';
+
+describe('PostsList', () => {
+ it('renders the correct number of posts', () => {
+ render(
+ <PostsList posts={posts} total={posts.length} searchPage={searchPage} />
+ );
+ expect(screen.getAllByRole('article')).toHaveLength(posts.length);
+ });
+
+ it('renders the number of loaded posts', () => {
+ render(
+ <PostsList posts={posts} total={posts.length} searchPage={searchPage} />
+ );
+ const info = `${posts.length} loaded articles out of a total of ${posts.length}`;
+ expect(screen.getByText(info)).toBeInTheDocument();
+ });
+
+ it('renders a load more button', () => {
+ render(
+ <PostsList
+ posts={posts}
+ total={posts.length}
+ showLoadMoreBtn={true}
+ searchPage={searchPage}
+ />
+ );
+ expect(
+ screen.getByRole('button', { name: /Load more/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a search form if no results', () => {
+ render(
+ <PostsList
+ posts={[]}
+ total={0}
+ showLoadMoreBtn={true}
+ searchPage={searchPage}
+ />
+ );
+ expect(screen.getByRole('searchbox')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
new file mode 100644
index 0000000..24869fd
--- /dev/null
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -0,0 +1,239 @@
+import Button from '@components/atoms/buttons/button';
+import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import ProgressBar from '@components/atoms/loaders/progress-bar';
+import Spinner from '@components/atoms/loaders/spinner';
+import Pagination, {
+ type PaginationProps,
+} from '@components/molecules/nav/pagination';
+import useIsMounted from '@utils/hooks/use-is-mounted';
+import useSettings from '@utils/hooks/use-settings';
+import { FC, Fragment, useRef } from 'react';
+import { useIntl } from 'react-intl';
+import NoResults, { NoResultsProps } from './no-results';
+import styles from './posts-list.module.scss';
+import Summary, { type SummaryProps } from './summary';
+
+export type Post = Omit<SummaryProps, 'titleLevel'> & {
+ /**
+ * The post id.
+ */
+ id: string | number;
+};
+
+export type YearCollection = {
+ [key: string]: Post[];
+};
+
+export type PostsListProps = Pick<PaginationProps, 'baseUrl' | 'siblings'> &
+ Pick<NoResultsProps, 'searchPage'> & {
+ /**
+ * True to display the posts by year. Default: false.
+ */
+ byYear?: boolean;
+ /**
+ * Determine if the data is loading.
+ */
+ isLoading?: boolean;
+ /**
+ * Load more button handler.
+ */
+ loadMore?: () => void;
+ /**
+ * The current page number. Default: 1.
+ */
+ pageNumber?: number;
+ /**
+ * The posts data.
+ */
+ posts: Post[];
+ /**
+ * Determine if the load more button should be visible.
+ */
+ showLoadMoreBtn?: boolean;
+ /**
+ * The posts heading level (hn).
+ */
+ titleLevel?: HeadingLevel;
+ /**
+ * The total posts number.
+ */
+ total: number;
+ };
+
+/**
+ * Create a collection of posts sorted by year.
+ *
+ * @param {Posts[]} data - A collection of posts.
+ * @returns {YearCollection} The posts sorted by year.
+ */
+const sortPostsByYear = (data: Post[]): YearCollection => {
+ const yearCollection: YearCollection = {};
+
+ data.forEach((post) => {
+ const postYear = new Date(post.meta.dates.publication)
+ .getFullYear()
+ .toString();
+ yearCollection[postYear] = [...(yearCollection[postYear] || []), post];
+ });
+
+ return yearCollection;
+};
+
+/**
+ * PostsList component
+ *
+ * Render a list of post summaries.
+ */
+const PostsList: FC<PostsListProps> = ({
+ baseUrl,
+ byYear = false,
+ isLoading = false,
+ loadMore,
+ pageNumber = 1,
+ posts,
+ searchPage,
+ showLoadMoreBtn = false,
+ siblings,
+ titleLevel,
+ total,
+}) => {
+ const intl = useIntl();
+ const listRef = useRef<HTMLOListElement>(null);
+ const lastPostRef = useRef<HTMLSpanElement>(null);
+ const isMounted = useIsMounted(listRef);
+ const { blog } = useSettings();
+
+ const lastPostId = posts.length ? posts[posts.length - 1].id : 0;
+
+ /**
+ * Retrieve the list of posts.
+ *
+ * @param {Posts[]} allPosts - A collection fo posts.
+ * @param {HeadingLevel} [headingLevel] - The posts heading level (hn).
+ * @returns {JSX.Element} The list of posts.
+ */
+ const getList = (
+ allPosts: Post[],
+ headingLevel: HeadingLevel = 2
+ ): JSX.Element => {
+ return (
+ <ol className={styles.list} ref={listRef}>
+ {allPosts.map(({ id, ...post }) => (
+ <Fragment key={id}>
+ <li className={styles.item}>
+ <Summary {...post} titleLevel={headingLevel} />
+ </li>
+ {id === lastPostId && (
+ <li>
+ <span ref={lastPostRef} tabIndex={-1} />
+ </li>
+ )}
+ </Fragment>
+ ))}
+ </ol>
+ );
+ };
+
+ /**
+ * Retrieve the list of posts.
+ *
+ * @returns {JSX.Element | JSX.Element[]} The posts list.
+ */
+ const getPosts = (): JSX.Element | JSX.Element[] => {
+ const firstLevel = titleLevel || 2;
+ if (!byYear) return getList(posts, firstLevel);
+
+ const postsPerYear = sortPostsByYear(posts);
+ const years = Object.keys(postsPerYear).reverse();
+ const nextLevel = (firstLevel + 1) as HeadingLevel;
+
+ return years.map((year) => {
+ return (
+ <section key={year} className={styles.section}>
+ <Heading level={firstLevel} className={styles.year}>
+ {year}
+ </Heading>
+ {getList(postsPerYear[year], nextLevel)}
+ </section>
+ );
+ });
+ };
+
+ const progressInfo = intl.formatMessage(
+ {
+ defaultMessage:
+ '{articlesCount, plural, =0 {# loaded articles} one {# loaded article} other {# loaded articles}} out of a total of {total}',
+ description: 'PostsList: loaded articles progress',
+ id: '9MeLN3',
+ },
+ { articlesCount: posts.length, total: total }
+ );
+
+ const loadMoreBody = intl.formatMessage({
+ defaultMessage: 'Load more articles?',
+ id: 'uaqd5F',
+ description: 'PostsList: load more button',
+ });
+
+ /**
+ * Load more posts handler.
+ */
+ const loadMorePosts = () => {
+ if (lastPostRef.current) {
+ lastPostRef.current.focus();
+ }
+
+ loadMore && loadMore();
+ };
+
+ const getProgressBar = () => {
+ return (
+ <>
+ <ProgressBar
+ min={1}
+ max={total}
+ current={posts.length}
+ info={progressInfo}
+ />
+ {showLoadMoreBtn && (
+ <Button
+ kind="tertiary"
+ onClick={loadMorePosts}
+ disabled={isLoading}
+ className={styles.btn}
+ >
+ {loadMoreBody}
+ </Button>
+ )}
+ </>
+ );
+ };
+
+ const getPagination = () => {
+ return posts.length <= blog.postsPerPage ? (
+ <Pagination
+ baseUrl={baseUrl}
+ current={pageNumber}
+ perPage={blog.postsPerPage}
+ siblings={siblings}
+ total={total}
+ />
+ ) : (
+ <></>
+ );
+ };
+
+ if (posts.length === 0) {
+ return <NoResults searchPage={searchPage} />;
+ }
+
+ return (
+ <>
+ {getPosts()}
+ {isLoading && <Spinner />}
+ {isMounted ? getProgressBar() : getPagination()}
+ </>
+ );
+};
+
+export default PostsList;
diff --git a/src/components/organisms/layout/summary.fixture.tsx b/src/components/organisms/layout/summary.fixture.tsx
new file mode 100644
index 0000000..bb3ebcb
--- /dev/null
+++ b/src/components/organisms/layout/summary.fixture.tsx
@@ -0,0 +1,25 @@
+import { type SummaryMeta } from './summary';
+
+export const cover = {
+ alt: 'A cover',
+ height: 480,
+ src: 'http://placeimg.com/640/480',
+ width: 640,
+};
+
+export const intro =
+ 'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.';
+
+export const meta: SummaryMeta = {
+ dates: { publication: '2022-04-11' },
+ wordsCount: intro.split(' ').length,
+ thematics: [
+ { id: 1, name: 'Cat 1', url: '#' },
+ { id: 2, name: 'Cat 2', url: '#' },
+ ],
+ commentsCount: 1,
+};
+
+export const title = 'Odio odit necessitatibus';
+
+export const url = '#';
diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss
new file mode 100644
index 0000000..62dfc0e
--- /dev/null
+++ b/src/components/organisms/layout/summary.module.scss
@@ -0,0 +1,121 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ column-gap: var(--spacing-md);
+ row-gap: var(--spacing-sm);
+ padding: var(--spacing-2xs) 0 var(--spacing-lg);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("xs") {
+ padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-md);
+ border: fun.convert-px(1) solid var(--color-primary-dark);
+ border-radius: fun.convert-px(3);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+ var(--color-shadow),
+ fun.convert-px(3) fun.convert-px(3) fun.convert-px(3) fun.convert-px(-1)
+ var(--color-shadow-light),
+ fun.convert-px(5) fun.convert-px(5) fun.convert-px(7) fun.convert-px(-1)
+ var(--color-shadow-light);
+ }
+
+ @include mix.dimensions("sm") {
+ grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
+ grid-template-rows: repeat(3, max-content);
+ }
+ }
+
+ &:hover {
+ .icon {
+ --icon-size: #{fun.convert-px(35)};
+
+ :global {
+ animation: pulse 1.5s ease-in-out 0.2s infinite;
+ }
+ }
+ }
+}
+
+.cover {
+ display: inline-flex;
+ flex-flow: column nowrap;
+ justify-content: center;
+ width: auto;
+ height: fun.convert-px(100);
+ max-width: 100%;
+ border: fun.convert-px(1) solid var(--color-border);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ }
+}
+
+.header {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ grid-column: 1;
+ grid-row: 1;
+ align-self: center;
+ }
+ }
+}
+
+.body {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ grid-column: 1;
+ grid-row: 2;
+ }
+ }
+}
+
+.footer {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ grid-column: 2;
+ grid-row: 2 / 4;
+ }
+ }
+}
+
+.link {
+ display: block;
+ width: fit-content;
+}
+
+.title {
+ margin: 0;
+ background: none;
+ color: inherit;
+ font-size: var(--font-size-2xl);
+ text-shadow: none;
+}
+
+.read-more {
+ display: flex;
+ flex-flow: row nowrap;
+ column-gap: var(--spacing-xs);
+ width: max-content;
+ margin: var(--spacing-sm) 0 0;
+}
+
+.meta {
+ flex-flow: row wrap;
+ font-size: var(--font-size-sm);
+
+ &__item {
+ flex: 1 0 min(calc(100vw - 2 * var(--spacing-md)), 14ch);
+ }
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ display: flex;
+ margin-top: 0;
+ }
+ }
+}
diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx
new file mode 100644
index 0000000..0b91e24
--- /dev/null
+++ b/src/components/organisms/layout/summary.stories.tsx
@@ -0,0 +1,107 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import Summary from './summary';
+import { cover, intro, meta } from './summary.fixture';
+
+/**
+ * Summary - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Layout/Summary',
+ component: Summary,
+ args: {
+ titleLevel: 2,
+ },
+ argTypes: {
+ cover: {
+ description: 'The cover data.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ excerpt: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page excerpt.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ meta: {
+ description: 'The page metadata.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page title',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ titleLevel: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The page title level (hn)',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 2 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ url: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Summary>;
+
+const Template: ComponentStory<typeof Summary> = (args) => (
+ <Summary {...args} />
+);
+
+/**
+ * Summary Stories - Default
+ */
+export const Default = Template.bind({});
+Default.args = {
+ intro,
+ meta,
+ title: 'Odio odit necessitatibus',
+ url: '#',
+};
+
+/**
+ * Summary Stories - With cover
+ */
+export const WithCover = Template.bind({});
+WithCover.args = {
+ intro,
+ meta: { ...meta, cover },
+ title: 'Odio odit necessitatibus',
+ url: '#',
+};
diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx
new file mode 100644
index 0000000..7617c26
--- /dev/null
+++ b/src/components/organisms/layout/summary.test.tsx
@@ -0,0 +1,54 @@
+import { render, screen } from '@test-utils';
+import Summary from './summary';
+import { cover, intro, meta, title, url } from './summary.fixture';
+
+describe('Summary', () => {
+ it('renders a title wrapped in a h2 element', () => {
+ render(
+ <Summary
+ intro={intro}
+ meta={meta}
+ title={title}
+ titleLevel={2}
+ url={url}
+ />
+ );
+ expect(
+ screen.getByRole('heading', { level: 2, name: title })
+ ).toBeInTheDocument();
+ });
+
+ it('renders an excerpt', () => {
+ render(<Summary intro={intro} meta={meta} title={title} url={url} />);
+ expect(screen.getByText(intro)).toBeInTheDocument();
+ });
+
+ it('renders a cover', () => {
+ render(
+ <Summary
+ intro={intro}
+ meta={{ ...meta, cover }}
+ title={title}
+ url={url}
+ />
+ );
+ expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+ });
+
+ it('renders a link to the full post', () => {
+ render(<Summary intro={intro} meta={meta} title={title} url={url} />);
+ expect(screen.getByRole('link', { name: title })).toBeInTheDocument();
+ });
+
+ it('renders a read more link', () => {
+ render(<Summary intro={intro} meta={meta} title={title} url={url} />);
+ expect(
+ screen.getByRole('link', { name: `Read more about ${title}` })
+ ).toBeInTheDocument();
+ });
+
+ it('renders some meta', () => {
+ render(<Summary intro={intro} meta={meta} title={title} url={url} />);
+ expect(screen.getByText(meta.thematics![0].name)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
new file mode 100644
index 0000000..8807878
--- /dev/null
+++ b/src/components/organisms/layout/summary.tsx
@@ -0,0 +1,136 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import Arrow from '@components/atoms/icons/arrow';
+import Link from '@components/atoms/links/link';
+import ResponsiveImage, {
+ type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
+import Meta, { type MetaData } from '@components/molecules/layout/meta';
+import { type Article, type Meta as MetaType } from '@ts/types/app';
+import useReadingTime from '@utils/hooks/use-reading-time';
+import { FC, ReactNode } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './summary.module.scss';
+
+export type Cover = Pick<
+ ResponsiveImageProps,
+ 'alt' | 'src' | 'width' | 'height'
+>;
+
+export type SummaryMeta = Pick<
+ MetaType<'article'>,
+ | 'author'
+ | 'commentsCount'
+ | 'cover'
+ | 'dates'
+ | 'thematics'
+ | 'topics'
+ | 'wordsCount'
+>;
+
+export type SummaryProps = Pick<Article, 'intro' | 'title'> & {
+ /**
+ * The post metadata.
+ */
+ meta: SummaryMeta;
+ /**
+ * The heading level (hn).
+ */
+ titleLevel?: HeadingLevel;
+ /**
+ * The post url.
+ */
+ url: string;
+};
+
+/**
+ * Summary component
+ *
+ * Render a page summary.
+ */
+const Summary: FC<SummaryProps> = ({
+ intro,
+ meta,
+ title,
+ titleLevel = 2,
+ url,
+}) => {
+ const intl = useIntl();
+ const readMore = intl.formatMessage(
+ {
+ defaultMessage: 'Read more<a11y> about {title}</a11y>',
+ description: 'Summary: read more link',
+ id: 'Zpgv+f',
+ },
+ {
+ title,
+ a11y: (chunks: ReactNode) => (
+ <span className="screen-reader-text">{chunks}</span>
+ ),
+ }
+ );
+ const { author, commentsCount, cover, dates, thematics, topics, wordsCount } =
+ meta;
+ const readingTime = useReadingTime(wordsCount, true);
+
+ const getMeta = (): MetaData => {
+ return {
+ author: author?.name,
+ publication: { date: dates.publication },
+ update:
+ dates.update && dates.publication !== dates.update
+ ? { date: dates.update }
+ : undefined,
+ readingTime,
+ thematics: thematics?.map((thematic) => (
+ <Link key={thematic.id} href={thematic.url}>
+ {thematic.name}
+ </Link>
+ )),
+ topics: topics?.map((topic) => (
+ <Link key={topic.id} href={topic.url}>
+ {topic.name}
+ </Link>
+ )),
+ comments: {
+ about: title,
+ count: commentsCount || 0,
+ target: `${url}#comments`,
+ },
+ };
+ };
+
+ return (
+ <article className={styles.wrapper}>
+ {cover && <ResponsiveImage className={styles.cover} {...cover} />}
+ <header className={styles.header}>
+ <Link href={url} className={styles.link}>
+ <Heading level={titleLevel} className={styles.title}>
+ {title}
+ </Heading>
+ </Link>
+ </header>
+ <div className={styles.body}>
+ <div dangerouslySetInnerHTML={{ __html: intro }} />
+ <ButtonLink target={url} className={styles['read-more']}>
+ <>
+ {readMore}
+ <Arrow direction="right" className={styles.icon} />
+ </>
+ </ButtonLink>
+ </div>
+ <footer className={styles.footer}>
+ <Meta
+ data={getMeta()}
+ layout="column"
+ itemsLayout="stacked"
+ withSeparator={false}
+ className={styles.meta}
+ groupClassName={styles.meta__item}
+ />
+ </footer>
+ </article>
+ );
+};
+
+export default Summary;
diff --git a/src/components/organisms/modals/search-modal.module.scss b/src/components/organisms/modals/search-modal.module.scss
new file mode 100644
index 0000000..aba0593
--- /dev/null
+++ b/src/components/organisms/modals/search-modal.module.scss
@@ -0,0 +1,11 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ padding-bottom: var(--spacing-md);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ max-width: 40ch;
+ }
+ }
+}
diff --git a/src/components/organisms/modals/search-modal.stories.tsx b/src/components/organisms/modals/search-modal.stories.tsx
new file mode 100644
index 0000000..5a607c6
--- /dev/null
+++ b/src/components/organisms/modals/search-modal.stories.tsx
@@ -0,0 +1,47 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SearchModal from './search-modal';
+
+/**
+ * SearchModal - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Modals',
+ component: SearchModal,
+ args: {
+ searchPage: '#',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the search modal wrapper.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ searchPage: {
+ control: {
+ type: 'text',
+ },
+ description: 'The search results page url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SearchModal>;
+
+const Template: ComponentStory<typeof SearchModal> = (args) => (
+ <SearchModal {...args} />
+);
+
+/**
+ * Modals Stories - Search
+ */
+export const Search = Template.bind({});
diff --git a/src/components/organisms/modals/search-modal.test.tsx b/src/components/organisms/modals/search-modal.test.tsx
new file mode 100644
index 0000000..7ba08c0
--- /dev/null
+++ b/src/components/organisms/modals/search-modal.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import SearchModal from './search-modal';
+
+describe('SearchModal', () => {
+ it('renders a search modal', () => {
+ render(<SearchModal searchPage="#" />);
+ expect(screen.getByText('Search')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx
new file mode 100644
index 0000000..ed6084a
--- /dev/null
+++ b/src/components/organisms/modals/search-modal.tsx
@@ -0,0 +1,37 @@
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
+import { useIntl } from 'react-intl';
+import SearchForm, { type SearchFormProps } from '../forms/search-form';
+import styles from './search-modal.module.scss';
+
+export type SearchModalProps = SearchFormProps & {
+ /**
+ * Set additional classnames to modal wrapper.
+ */
+ className?: ModalProps['className'];
+};
+
+/**
+ * SearchModal
+ *
+ * Render a search form modal.
+ */
+const SearchModal: ForwardRefRenderFunction<
+ HTMLInputElement,
+ SearchModalProps
+> = ({ className, searchPage }, ref) => {
+ const intl = useIntl();
+ const modalTitle = intl.formatMessage({
+ defaultMessage: 'Search',
+ description: 'SearchModal: modal title',
+ id: 'G+Twgm',
+ });
+
+ return (
+ <Modal title={modalTitle} className={`${styles.wrapper} ${className}`}>
+ <SearchForm hideLabel={true} ref={ref} searchPage={searchPage} />
+ </Modal>
+ );
+};
+
+export default forwardRef(SearchModal);
diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss
new file mode 100644
index 0000000..a6a2077
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.module.scss
@@ -0,0 +1,11 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.label {
+ margin-right: auto;
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "2xs", "height") {
+ font-size: var(--font-size-sm);
+ }
+ }
+}
diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx
new file mode 100644
index 0000000..d263e2b
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.stories.tsx
@@ -0,0 +1,67 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SettingsModal from './settings-modal';
+
+/**
+ * SettingsModal - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Modals',
+ component: SettingsModal,
+ argTypes: {
+ ackeeStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'A local storage key for Ackee.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ motionStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'A local storage key for reduced motion setting..',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ tooltipClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof SettingsModal>;
+
+const Template: ComponentStory<typeof SettingsModal> = (args) => (
+ <SettingsModal {...args} />
+);
+
+/**
+ * Modals Stories - Settings
+ */
+export const Settings = Template.bind({});
diff --git a/src/components/organisms/modals/settings-modal.test.tsx b/src/components/organisms/modals/settings-modal.test.tsx
new file mode 100644
index 0000000..d6ed989
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.test.tsx
@@ -0,0 +1,14 @@
+import { render, screen } from '@test-utils';
+import SettingsModal from './settings-modal';
+
+describe('SettingsModal', () => {
+ it('renders a fake heading', () => {
+ render(
+ <SettingsModal
+ ackeeStorageKey="ackee-tracking"
+ motionStorageKey="reduce-motion"
+ />
+ );
+ expect(screen.getByText(/Settings/i)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx
new file mode 100644
index 0000000..5d14836
--- /dev/null
+++ b/src/components/organisms/modals/settings-modal.tsx
@@ -0,0 +1,51 @@
+import Spinner from '@components/atoms/loaders/spinner';
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import dynamic from 'next/dynamic';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import { type SettingsFormProps } from '../forms/settings-form';
+import styles from './settings-modal.module.scss';
+
+const DynamicSettingsForm = dynamic(
+ () => import('@components/organisms/forms/settings-form'),
+ {
+ loading: () => <Spinner />,
+ ssr: false,
+ }
+);
+
+export type SettingsModalProps = Pick<ModalProps, 'className'> &
+ Pick<
+ SettingsFormProps,
+ 'ackeeStorageKey' | 'motionStorageKey' | 'tooltipClassName'
+ >;
+
+/**
+ * SettingsModal component
+ *
+ * Render a modal with settings options.
+ */
+const SettingsModal: FC<SettingsModalProps> = ({
+ className = '',
+ ...props
+}) => {
+ const intl = useIntl();
+ const title = intl.formatMessage({
+ defaultMessage: 'Settings',
+ description: 'SettingsModal: title',
+ id: 'gPfT/K',
+ });
+
+ return (
+ <Modal
+ title={title}
+ icon="cogs"
+ className={`${styles.wrapper} ${className}`}
+ headingClassName={styles.heading}
+ >
+ <DynamicSettingsForm {...props} />
+ </Modal>
+ );
+};
+
+export default SettingsModal;
diff --git a/src/components/organisms/toolbar/main-nav.module.scss b/src/components/organisms/toolbar/main-nav.module.scss
new file mode 100644
index 0000000..24abc43
--- /dev/null
+++ b/src/components/organisms/toolbar/main-nav.module.scss
@@ -0,0 +1,96 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.item {
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ .checkbox,
+ .label {
+ display: none;
+ }
+
+ .modal {
+ position: relative;
+ }
+ }
+ }
+
+ .modal {
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "md") {
+ padding: var(--spacing-2xs);
+ background: var(--color-bg-secondary);
+ border-top: fun.convert-px(4) solid;
+ border-bottom: fun.convert-px(4) solid;
+ border-image: radial-gradient(
+ ellipse at top,
+ var(--color-primary-lighter) 20%,
+ var(--color-primary) 100%
+ )
+ 1;
+ box-shadow: fun.convert-px(2) fun.convert-px(-2) fun.convert-px(3)
+ fun.convert-px(-1) var(--color-shadow-dark);
+ }
+
+ @include mix.dimensions("sm", "md") {
+ border-left: fun.convert-px(4) solid;
+ border-right: fun.convert-px(4) solid;
+ }
+
+ @include mix.dimensions("md") {
+ top: unset;
+ }
+ }
+ }
+
+ .modal__list {
+ display: flex;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm", "md") {
+ flex-flow: column;
+ }
+ }
+ }
+
+ .checkbox {
+ &:checked {
+ ~ .label .icon {
+ background: transparent;
+ border: transparent;
+
+ &::before {
+ top: 0;
+ transform-origin: 50% 50%;
+ transform: rotate(-45deg);
+ }
+
+ &::after {
+ bottom: 0;
+ transform-origin: 50% 50%;
+ transform: rotate(45deg);
+ }
+ }
+ }
+
+ &:not(:checked) {
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ ~ .modal {
+ opacity: 1;
+ visibility: visible;
+ transform: none;
+ }
+ }
+ }
+ }
+ }
+}
+
+.label {
+ display: flex;
+ place-content: center;
+ place-items: center;
+ width: var(--btn-size, #{fun.convert-px(60)});
+ height: var(--btn-size, #{fun.convert-px(60)});
+}
diff --git a/src/components/organisms/toolbar/main-nav.stories.tsx b/src/components/organisms/toolbar/main-nav.stories.tsx
new file mode 100644
index 0000000..831636f
--- /dev/null
+++ b/src/components/organisms/toolbar/main-nav.stories.tsx
@@ -0,0 +1,91 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import MainNav from './main-nav';
+
+/**
+ * MainNav - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Toolbar/MainNav',
+ component: MainNav,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the main nav wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isActive: {
+ control: {
+ type: null,
+ },
+ description: 'Determine if the main nav is open or not.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ items: {
+ description: 'The main nav items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ setIsActive: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to change main nav state.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof MainNav>;
+
+const Template: ComponentStory<typeof MainNav> = ({
+ isActive,
+ setIsActive: _setIsActive,
+ ...args
+}) => {
+ const [isOpen, setIsOpen] = useState<boolean>(isActive);
+
+ return <MainNav isActive={isOpen} setIsActive={setIsOpen} {...args} />;
+};
+
+/**
+ * MainNav Stories - Inactive
+ */
+export const Inactive = Template.bind({});
+Inactive.args = {
+ isActive: false,
+ items: [
+ { id: 'home', label: 'Home', href: '#' },
+ { id: 'contact', label: 'Contact', href: '#' },
+ ],
+};
+
+/**
+ * MainNav Stories - Active
+ */
+export const Active = Template.bind({});
+Active.args = {
+ isActive: true,
+ items: [
+ { id: 'home', label: 'Home', href: '#' },
+ { id: 'contact', label: 'Contact', href: '#' },
+ ],
+};
diff --git a/src/components/organisms/toolbar/main-nav.test.tsx b/src/components/organisms/toolbar/main-nav.test.tsx
new file mode 100644
index 0000000..6e50562
--- /dev/null
+++ b/src/components/organisms/toolbar/main-nav.test.tsx
@@ -0,0 +1,33 @@
+import { render, screen } from '@test-utils';
+import MainNav from './main-nav';
+
+const items = [
+ { id: 'home', label: 'Home', href: '/' },
+ { id: 'blog', label: 'Blog', href: '/blog' },
+ { id: 'contact', label: 'Contact', href: '/contact' },
+];
+
+describe('MainNav', () => {
+ it('renders a checkbox to open main nav', () => {
+ render(<MainNav items={items} isActive={false} setIsActive={() => null} />);
+ expect(screen.getByRole('checkbox')).toHaveAccessibleName('Open menu');
+ });
+
+ it('renders a checkbox to close main nav', () => {
+ render(<MainNav items={items} isActive={true} setIsActive={() => null} />);
+ expect(screen.getByRole('checkbox')).toHaveAccessibleName('Close menu');
+ });
+
+ it('renders the correct number of items', () => {
+ render(<MainNav items={items} isActive={true} setIsActive={() => null} />);
+ expect(screen.getAllByRole('listitem')).toHaveLength(items.length);
+ });
+
+ it('renders some links with the right label', () => {
+ render(<MainNav items={items} isActive={true} setIsActive={() => null} />);
+ expect(screen.getByRole('link', { name: items[0].label })).toHaveAttribute(
+ 'href',
+ items[0].href
+ );
+ });
+});
diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx
new file mode 100644
index 0000000..d205112
--- /dev/null
+++ b/src/components/organisms/toolbar/main-nav.tsx
@@ -0,0 +1,80 @@
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
+import Label from '@components/atoms/forms/label';
+import Hamburger from '@components/atoms/icons/hamburger';
+import Nav, {
+ type NavProps,
+ type NavItem,
+} from '@components/molecules/nav/nav';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
+import { useIntl } from 'react-intl';
+import mainNavStyles from './main-nav.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
+
+export type MainNavProps = {
+ /**
+ * Set additional classnames to the nav element.
+ */
+ className?: NavProps['className'];
+ /**
+ * The button state.
+ */
+ isActive: CheckboxProps['value'];
+ /**
+ * The main nav items.
+ */
+ items: NavItem[];
+ /**
+ * A callback function to handle button state.
+ */
+ setIsActive: CheckboxProps['setValue'];
+};
+
+/**
+ * MainNav component
+ *
+ * Render the main navigation.
+ */
+const MainNav: ForwardRefRenderFunction<HTMLDivElement, MainNavProps> = (
+ { className = '', isActive, items, setIsActive },
+ ref
+) => {
+ const intl = useIntl();
+ const label = isActive
+ ? intl.formatMessage({
+ defaultMessage: 'Close menu',
+ description: 'MainNav: Close label',
+ id: 'aJC7D2',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'Open menu',
+ description: 'MainNav: Open label',
+ id: 'GTbGMy',
+ });
+
+ return (
+ <div className={`${sharedStyles.item} ${mainNavStyles.item}`} ref={ref}>
+ <Checkbox
+ id="main-nav-button"
+ name="main-nav-button"
+ value={isActive}
+ setValue={setIsActive}
+ className={`${sharedStyles.checkbox} ${mainNavStyles.checkbox}`}
+ />
+ <Label
+ htmlFor="main-nav-button"
+ aria-label={label}
+ className={`${sharedStyles.label} ${mainNavStyles.label}`}
+ >
+ <Hamburger iconClassName={mainNavStyles.icon} />
+ </Label>
+ <Nav
+ kind="main"
+ items={items}
+ className={`${sharedStyles.modal} ${mainNavStyles.modal} ${className}`}
+ listClassName={mainNavStyles.modal__list}
+ />
+ </div>
+ );
+};
+
+export default forwardRef(MainNav);
diff --git a/src/components/organisms/toolbar/search.module.scss b/src/components/organisms/toolbar/search.module.scss
new file mode 100644
index 0000000..c310594
--- /dev/null
+++ b/src/components/organisms/toolbar/search.module.scss
@@ -0,0 +1,3 @@
+.modal {
+ padding-bottom: var(--spacing-md);
+}
diff --git a/src/components/organisms/toolbar/search.stories.tsx b/src/components/organisms/toolbar/search.stories.tsx
new file mode 100644
index 0000000..f0f65b4
--- /dev/null
+++ b/src/components/organisms/toolbar/search.stories.tsx
@@ -0,0 +1,88 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import Search from './search';
+
+/**
+ * Search - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Toolbar/Search',
+ component: Search,
+ args: {
+ searchPage: '#',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isActive: {
+ control: {
+ type: null,
+ },
+ description: 'Define the modal state: either opened or closed.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ searchPage: {
+ control: {
+ type: 'text',
+ },
+ description: 'The search results page url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ setIsActive: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to update modal state.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof Search>;
+
+const Template: ComponentStory<typeof Search> = ({
+ isActive,
+ setIsActive: _setIsActive,
+ ...args
+}) => {
+ const [isOpen, setIsOpen] = useState<boolean>(isActive);
+
+ return <Search isActive={isOpen} setIsActive={setIsOpen} {...args} />;
+};
+
+/**
+ * Search Stories - Inactive
+ */
+export const Inactive = Template.bind({});
+Inactive.args = {
+ isActive: false,
+};
+
+/**
+ * Search Stories - Active
+ */
+export const Active = Template.bind({});
+Active.args = {
+ isActive: true,
+};
diff --git a/src/components/organisms/toolbar/search.test.tsx b/src/components/organisms/toolbar/search.test.tsx
new file mode 100644
index 0000000..7c77eac
--- /dev/null
+++ b/src/components/organisms/toolbar/search.test.tsx
@@ -0,0 +1,14 @@
+import { render, screen } from '@test-utils';
+import Search from './search';
+
+describe('Search', () => {
+ it('renders a button to open search modal', () => {
+ render(<Search searchPage="#" isActive={false} setIsActive={() => null} />);
+ expect(screen.getByRole('checkbox')).toHaveAccessibleName('Open search');
+ });
+
+ it('renders a button to close search modal', () => {
+ render(<Search searchPage="#" isActive={true} setIsActive={() => null} />);
+ expect(screen.getByRole('checkbox')).toHaveAccessibleName('Close search');
+ });
+});
diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx
new file mode 100644
index 0000000..6a8af26
--- /dev/null
+++ b/src/components/organisms/toolbar/search.tsx
@@ -0,0 +1,80 @@
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
+import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
+import FlippingLabel from '@components/molecules/forms/flipping-label';
+import useInputAutofocus from '@utils/hooks/use-input-autofocus';
+import { forwardRef, ForwardRefRenderFunction, useRef } from 'react';
+import { useIntl } from 'react-intl';
+import SearchModal, { type SearchModalProps } from '../modals/search-modal';
+import searchStyles from './search.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
+
+export type SearchProps = {
+ /**
+ * Set additional classnames to the modal wrapper.
+ */
+ className?: SearchModalProps['className'];
+ /**
+ * The button state.
+ */
+ isActive: CheckboxProps['value'];
+ /**
+ * A callback function to execute search.
+ */
+ searchPage: SearchModalProps['searchPage'];
+ /**
+ * A callback function to handle button state.
+ */
+ setIsActive: CheckboxProps['setValue'];
+};
+
+const Search: ForwardRefRenderFunction<HTMLDivElement, SearchProps> = (
+ { className = '', isActive, searchPage, setIsActive },
+ ref
+) => {
+ const intl = useIntl();
+ const label = isActive
+ ? intl.formatMessage({
+ defaultMessage: 'Close search',
+ id: 'LDDUNO',
+ description: 'Search: Close label',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'Open search',
+ id: 'Xj+WXB',
+ description: 'Search: Open label',
+ });
+
+ const searchInputRef = useRef<HTMLInputElement>(null);
+ useInputAutofocus({
+ condition: isActive,
+ delay: 360,
+ ref: searchInputRef,
+ });
+
+ return (
+ <div className={`${sharedStyles.item} ${searchStyles.item}`} ref={ref}>
+ <Checkbox
+ id="search-button"
+ name="search-button"
+ value={isActive}
+ setValue={setIsActive}
+ className={`${sharedStyles.checkbox} ${searchStyles.checkbox}`}
+ />
+ <FlippingLabel
+ className={sharedStyles.label}
+ htmlFor="search-button"
+ aria-label={label}
+ isActive={isActive}
+ >
+ <MagnifyingGlass />
+ </FlippingLabel>
+ <SearchModal
+ className={`${sharedStyles.modal} ${searchStyles.modal} ${className}`}
+ ref={searchInputRef}
+ searchPage={searchPage}
+ />
+ </div>
+ );
+};
+
+export default forwardRef(Search);
diff --git a/src/components/organisms/toolbar/settings.module.scss b/src/components/organisms/toolbar/settings.module.scss
new file mode 100644
index 0000000..08c8cd4
--- /dev/null
+++ b/src/components/organisms/toolbar/settings.module.scss
@@ -0,0 +1,10 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.modal {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ width: 120%;
+ }
+ }
+}
diff --git a/src/components/organisms/toolbar/settings.stories.tsx b/src/components/organisms/toolbar/settings.stories.tsx
new file mode 100644
index 0000000..08ec579
--- /dev/null
+++ b/src/components/organisms/toolbar/settings.stories.tsx
@@ -0,0 +1,112 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { useState } from 'react';
+import Settings from './settings';
+
+/**
+ * Settings - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Toolbar/Settings',
+ component: Settings,
+ args: {
+ ackeeStorageKey: 'ackee-tracking',
+ motionStorageKey: 'reduced-motion',
+ },
+ argTypes: {
+ ackeeStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set Ackee settings local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the modal wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ isActive: {
+ control: {
+ type: null,
+ },
+ description: 'Define the modal state: either opened or closed.',
+ table: {
+ category: 'Events',
+ },
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ motionStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set Reduced motion settings local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ setIsActive: {
+ control: {
+ type: null,
+ },
+ description: 'A callback function to update modal state.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ tooltipClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the tooltip wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Settings>;
+
+const Template: ComponentStory<typeof Settings> = ({
+ isActive,
+ setIsActive: _setIsActive,
+ ...args
+}) => {
+ const [isOpen, setIsOpen] = useState<boolean>(isActive);
+
+ return <Settings isActive={isOpen} setIsActive={setIsOpen} {...args} />;
+};
+
+/**
+ * Settings Stories - Inactive
+ */
+export const Inactive = Template.bind({});
+Inactive.args = {
+ isActive: false,
+};
+
+/**
+ * Settings Stories - Active
+ */
+export const Active = Template.bind({});
+Active.args = {
+ isActive: true,
+};
diff --git a/src/components/organisms/toolbar/settings.test.tsx b/src/components/organisms/toolbar/settings.test.tsx
new file mode 100644
index 0000000..7ccb234
--- /dev/null
+++ b/src/components/organisms/toolbar/settings.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen } from '@test-utils';
+import Settings from './settings';
+
+describe('Settings', () => {
+ it('renders a button to open settings modal', () => {
+ render(
+ <Settings
+ ackeeStorageKey="ackee-tracking"
+ motionStorageKey="reduced-motion"
+ isActive={false}
+ setIsActive={() => null}
+ />
+ );
+ expect(
+ screen.getByRole('checkbox', { name: 'Open settings' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders a button to close settings modal', () => {
+ render(
+ <Settings
+ ackeeStorageKey="ackee-tracking"
+ motionStorageKey="reduced-motion"
+ isActive={true}
+ setIsActive={() => null}
+ />
+ );
+ expect(
+ screen.getByRole('checkbox', { name: 'Close settings' })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx
new file mode 100644
index 0000000..ceb6db4
--- /dev/null
+++ b/src/components/organisms/toolbar/settings.tsx
@@ -0,0 +1,74 @@
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
+import Cog from '@components/atoms/icons/cog';
+import FlippingLabel from '@components/molecules/forms/flipping-label';
+import { forwardRef, ForwardRefRenderFunction } from 'react';
+import { useIntl } from 'react-intl';
+import SettingsModal, {
+ type SettingsModalProps,
+} from '../modals/settings-modal';
+import settingsStyles from './settings.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
+
+export type SettingsProps = SettingsModalProps & {
+ /**
+ * The button state.
+ */
+ isActive: CheckboxProps['value'];
+ /**
+ * A callback function to handle button state.
+ */
+ setIsActive: CheckboxProps['setValue'];
+};
+
+const Settings: ForwardRefRenderFunction<HTMLDivElement, SettingsProps> = (
+ {
+ ackeeStorageKey,
+ className = '',
+ isActive,
+ motionStorageKey,
+ setIsActive,
+ tooltipClassName = '',
+ },
+ ref
+) => {
+ const intl = useIntl();
+ const label = isActive
+ ? intl.formatMessage({
+ defaultMessage: 'Close settings',
+ id: '+viX9b',
+ description: 'Settings: Close label',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'Open settings',
+ id: 'QCW3cy',
+ description: 'Settings: Open label',
+ });
+
+ return (
+ <div className={`${sharedStyles.item} ${settingsStyles.item}`} ref={ref}>
+ <Checkbox
+ id="settings-button"
+ name="settings-button"
+ value={isActive}
+ setValue={setIsActive}
+ className={`${sharedStyles.checkbox} ${settingsStyles.checkbox}`}
+ />
+ <FlippingLabel
+ className={sharedStyles.label}
+ htmlFor="settings-button"
+ aria-label={label}
+ isActive={isActive}
+ >
+ <Cog />
+ </FlippingLabel>
+ <SettingsModal
+ ackeeStorageKey={ackeeStorageKey}
+ className={`${sharedStyles.modal} ${settingsStyles.modal} ${className}`}
+ motionStorageKey={motionStorageKey}
+ tooltipClassName={tooltipClassName}
+ />
+ </div>
+ );
+};
+
+export default forwardRef(Settings);
diff --git a/src/components/organisms/toolbar/toolbar-items.module.scss b/src/components/organisms/toolbar/toolbar-items.module.scss
new file mode 100644
index 0000000..86b4924
--- /dev/null
+++ b/src/components/organisms/toolbar/toolbar-items.module.scss
@@ -0,0 +1,91 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.item {
+ --btn-size: #{fun.convert-px(65)};
+
+ display: flex;
+ position: relative;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ justify-content: flex-end;
+ }
+
+ @include mix.dimensions("md") {
+ justify-content: flex-end;
+ }
+ }
+}
+
+.modal {
+ position: absolute;
+ top: var(--toolbar-size, calc(var(--btn-size) + var(--spacing-2xs)));
+ transition: all 0.8s ease-in-out 0s, background 0s;
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "sm") {
+ position: fixed;
+ left: 0;
+ right: 0;
+ }
+ }
+}
+
+.label {
+ --draw-border-thickness: #{fun.convert-px(4)};
+ --draw-border-color1: var(--color-primary-light);
+ --draw-border-color2: var(--color-primary-lighter);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ border-radius: fun.convert-px(5);
+ }
+ }
+
+ &:hover {
+ @extend %draw-borders;
+ }
+
+ &:active {
+ --draw-border-color1: var(--color-primary-dark);
+ --draw-border-color2: var(--color-primary-light);
+
+ @extend %draw-borders;
+ }
+}
+
+.checkbox {
+ position: absolute;
+ top: calc(var(--btn-size) / 2);
+ left: calc(var(--btn-size) / 2);
+ opacity: 0;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ ~ .label {
+ @extend %draw-borders;
+ }
+ }
+
+ &:not(:checked) {
+ ~ .modal {
+ opacity: 0;
+ visibility: hidden;
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "sm") {
+ transform: translateX(-100vw);
+ }
+
+ @include mix.dimensions("sm") {
+ transform: perspective(#{fun.convert-px(400)})
+ translate3d(0, 0, #{fun.convert-px(-400)});
+ transform-origin: 100% -50%;
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/organisms/toolbar/toolbar.module.scss b/src/components/organisms/toolbar/toolbar.module.scss
new file mode 100644
index 0000000..4bcabcb
--- /dev/null
+++ b/src/components/organisms/toolbar/toolbar.module.scss
@@ -0,0 +1,98 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.wrapper {
+ --toolbar-size: #{fun.convert-px(75)};
+
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-around;
+ width: 100%;
+ height: var(--toolbar-size);
+ position: relative;
+ background: var(--color-bg);
+ border-top: fun.convert-px(4) solid;
+ border-image: radial-gradient(
+ ellipse at top,
+ var(--color-primary-lighter) 20%,
+ var(--color-primary) 100%
+ )
+ 1;
+ box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1)
+ var(--color-shadow-dark);
+
+ :global {
+ animation: slide-in-from-bottom 0.8s ease-in-out 0s 1;
+ }
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ :global {
+ animation: slide-in-from-top 1s ease-in-out 0s 1;
+ }
+ }
+ }
+
+ .modal {
+ &--search,
+ &--settings {
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ min-width: 35ch;
+ }
+ }
+ }
+ }
+
+ @include mix.media("screen") {
+ @include mix.dimensions(null, "2xs", "height") {
+ --toolbar-size: #{fun.convert-px(70)};
+ }
+
+ @include mix.dimensions(null, "sm") {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: 5;
+
+ .modal {
+ top: unset;
+ bottom: calc(var(--toolbar-size) - #{fun.convert-px(4)});
+ max-height: calc(100vh - var(--toolbar-size));
+ }
+
+ .tooltip {
+ padding: calc(var(--title-height) / 2 + var(--spacing-2xs))
+ var(--spacing-2xs) var(--spacing-2xs);
+ top: unset;
+ bottom: calc(100% + var(--spacing-2xs));
+ transform-origin: bottom right;
+ }
+ }
+
+ @include mix.dimensions("sm", "md") {
+ .modal {
+ top: calc(var(--toolbar-size) + var(--spacing-2xs));
+ bottom: unset;
+ }
+ }
+
+ @include mix.dimensions("sm") {
+ justify-content: flex-end;
+ gap: var(--spacing-sm);
+
+ .tooltip {
+ transform-origin: top right;
+ }
+ }
+
+ @include mix.dimensions("md") {
+ .tooltip {
+ width: 120%;
+ right: -10%;
+ }
+ }
+ }
+}
diff --git a/src/components/organisms/toolbar/toolbar.stories.tsx b/src/components/organisms/toolbar/toolbar.stories.tsx
new file mode 100644
index 0000000..d613442
--- /dev/null
+++ b/src/components/organisms/toolbar/toolbar.stories.tsx
@@ -0,0 +1,90 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ToolbarComponent from './toolbar';
+
+/**
+ * Toolbar - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Toolbar',
+ component: ToolbarComponent,
+ args: {
+ ackeeStorageKey: 'ackee-tracking',
+ motionStorageKey: 'reduced-motion',
+ searchPage: '#',
+ },
+ argTypes: {
+ ackeeStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set Ackee settings local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the toolbar wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ motionStorageKey: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set Reduced motion settings local storage key.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ nav: {
+ description: 'The main nav items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ searchPage: {
+ control: {
+ type: 'text',
+ },
+ description: 'The search results page url.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof ToolbarComponent>;
+
+const Template: ComponentStory<typeof ToolbarComponent> = (args) => (
+ <ToolbarComponent {...args} />
+);
+
+const nav = [
+ { id: 'home-link', href: '#', label: 'Home' },
+ { id: 'blog-link', href: '#', label: 'Blog' },
+ { id: 'cv-link', href: '#', label: 'CV' },
+ { id: 'contact-link', href: '#', label: 'Contact' },
+];
+
+/**
+ * Toolbar Story
+ */
+export const Toolbar = Template.bind({});
+Toolbar.args = {
+ nav,
+};
diff --git a/src/components/organisms/toolbar/toolbar.test.tsx b/src/components/organisms/toolbar/toolbar.test.tsx
new file mode 100644
index 0000000..72965e8
--- /dev/null
+++ b/src/components/organisms/toolbar/toolbar.test.tsx
@@ -0,0 +1,23 @@
+import { render, screen } from '@test-utils';
+import Toolbar from './toolbar';
+
+const nav = [
+ { id: 'home-link', href: '/', label: 'Home' },
+ { id: 'blog-link', href: '/blog', label: 'Blog' },
+ { id: 'cv-link', href: '/cv', label: 'CV' },
+ { id: 'contact-link', href: '/contact', label: 'Contact' },
+];
+
+describe('Toolbar', () => {
+ it('renders a navigation menu', () => {
+ render(
+ <Toolbar
+ ackeeStorageKey="ackee-tracking"
+ motionStorageKey="reduced-motion"
+ nav={nav}
+ searchPage="#"
+ />
+ );
+ expect(screen.getByRole('navigation')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx
new file mode 100644
index 0000000..ee61a7b
--- /dev/null
+++ b/src/components/organisms/toolbar/toolbar.tsx
@@ -0,0 +1,77 @@
+import useClickOutside from '@utils/hooks/use-click-outside';
+import useRouteChange from '@utils/hooks/use-route-change';
+import { FC, useRef, useState } from 'react';
+import MainNav, { type MainNavProps } from '../toolbar/main-nav';
+import Search, { type SearchProps } from '../toolbar/search';
+import Settings, { SettingsProps } from '../toolbar/settings';
+import styles from './toolbar.module.scss';
+
+export type ToolbarProps = Pick<SearchProps, 'searchPage'> &
+ Pick<SettingsProps, 'ackeeStorageKey' | 'motionStorageKey'> & {
+ /**
+ * Set additional classnames to the toolbar wrapper.
+ */
+ className?: string;
+ /**
+ * The main nav items.
+ */
+ nav: MainNavProps['items'];
+ };
+
+/**
+ * Toolbar component
+ *
+ * Render the website toolbar.
+ */
+const Toolbar: FC<ToolbarProps> = ({
+ ackeeStorageKey,
+ className = '',
+ motionStorageKey,
+ nav,
+ searchPage,
+}) => {
+ const [isNavOpened, setIsNavOpened] = useState<boolean>(false);
+ const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false);
+ const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false);
+ const mainNavRef = useRef<HTMLDivElement>(null);
+ const searchRef = useRef<HTMLDivElement>(null);
+ const settingsRef = useRef<HTMLDivElement>(null);
+
+ useClickOutside(mainNavRef, () => isNavOpened && setIsNavOpened(false));
+ useClickOutside(searchRef, () => isSearchOpened && setIsSearchOpened(false));
+ useClickOutside(
+ settingsRef,
+ () => isSettingsOpened && setIsSettingsOpened(false)
+ );
+ useRouteChange(() => setIsSearchOpened(false));
+
+ return (
+ <div className={`${styles.wrapper} ${className}`}>
+ <MainNav
+ items={nav}
+ isActive={isNavOpened}
+ setIsActive={setIsNavOpened}
+ className={styles.modal}
+ ref={mainNavRef}
+ />
+ <Search
+ searchPage={searchPage}
+ isActive={isSearchOpened}
+ setIsActive={setIsSearchOpened}
+ className={`${styles.modal} ${styles['modal--search']}`}
+ ref={searchRef}
+ />
+ <Settings
+ ackeeStorageKey={ackeeStorageKey}
+ className={`${styles.modal} ${styles['modal--settings']}`}
+ isActive={isSettingsOpened}
+ motionStorageKey={motionStorageKey}
+ ref={settingsRef}
+ setIsActive={setIsSettingsOpened}
+ tooltipClassName={styles.tooltip}
+ />
+ </div>
+ );
+};
+
+export default Toolbar;
diff --git a/src/components/organisms/widgets/image-widget.module.scss b/src/components/organisms/widgets/image-widget.module.scss
new file mode 100644
index 0000000..0d69441
--- /dev/null
+++ b/src/components/organisms/widgets/image-widget.module.scss
@@ -0,0 +1,47 @@
+@use "@styles/abstracts/functions" as fun;
+
+.figure {
+ --scale-up: 1.02;
+ --scale-down: 0.98;
+
+ margin: 0;
+ padding: fun.convert-px(5);
+ border: fun.convert-px(1) solid var(--color-border);
+}
+
+.txt {
+ padding: var(--spacing-sm);
+}
+
+.widget {
+ &--left {
+ .figure {
+ margin-right: auto;
+ }
+
+ .txt {
+ text-align: left;
+ }
+ }
+
+ &--center {
+ .figure {
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ .txt {
+ text-align: center;
+ }
+ }
+
+ &--right {
+ .figure {
+ margin-left: auto;
+ }
+
+ .txt {
+ text-align: right;
+ }
+ }
+}
diff --git a/src/components/organisms/widgets/image-widget.stories.tsx b/src/components/organisms/widgets/image-widget.stories.tsx
new file mode 100644
index 0000000..2271c03
--- /dev/null
+++ b/src/components/organisms/widgets/image-widget.stories.tsx
@@ -0,0 +1,181 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ImageWidget from './image-widget';
+
+/**
+ * ImageWidget - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Widgets/Image',
+ component: ImageWidget,
+ args: {
+ alignment: 'left',
+ },
+ argTypes: {
+ alignment: {
+ control: {
+ type: 'select',
+ },
+ description: 'The content alignment.',
+ options: ['left', 'center', 'right'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'left' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the widget wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ description: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add a caption image.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ expanded: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'The state of the widget.',
+ type: {
+ name: 'boolean',
+ required: true,
+ },
+ },
+ image: {
+ description: 'An image object.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ imageClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the image wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ level: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The widget title level (hn).',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The widget title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ url: {
+ control: {
+ type: 'text',
+ },
+ description: 'Add a link to the image.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof ImageWidget>;
+
+const Template: ComponentStory<typeof ImageWidget> = (args) => (
+ <ImageWidget {...args} />
+);
+
+const image = {
+ alt: 'Et perferendis quaerat',
+ height: 480,
+ src: 'http://placeimg.com/640/480/nature',
+ width: 640,
+};
+
+/**
+ * ImageWidget Stories - Align left
+ */
+export const AlignLeft = Template.bind({});
+AlignLeft.args = {
+ alignment: 'left',
+ expanded: true,
+ image,
+ level: 2,
+ title: 'Quo et totam',
+};
+
+/**
+ * ImageWidget Stories - Align center
+ */
+export const AlignCenter = Template.bind({});
+AlignCenter.args = {
+ alignment: 'center',
+ expanded: true,
+ image,
+ level: 2,
+ title: 'Quo et totam',
+};
+
+/**
+ * ImageWidget Stories - Align right
+ */
+export const AlignRight = Template.bind({});
+AlignRight.args = {
+ alignment: 'right',
+ expanded: true,
+ image,
+ level: 2,
+ title: 'Quo et totam',
+};
+
+/**
+ * ImageWidget Stories - With description
+ */
+export const WithDescription = Template.bind({});
+WithDescription.args = {
+ description: 'Sint enim harum',
+ expanded: true,
+ image,
+ level: 2,
+ title: 'Quo et totam',
+};
diff --git a/src/components/organisms/widgets/image-widget.test.tsx b/src/components/organisms/widgets/image-widget.test.tsx
new file mode 100644
index 0000000..c6b1a3a
--- /dev/null
+++ b/src/components/organisms/widgets/image-widget.test.tsx
@@ -0,0 +1,59 @@
+import { render, screen } from '@test-utils';
+import ImageWidget from './image-widget';
+
+const description = 'Ut vitae sit';
+
+const img = {
+ alt: 'Et perferendis quaerat',
+ height: 480,
+ src: 'http://placeimg.com/640/480/nature',
+ width: 640,
+};
+
+const title = 'Fugiat cumque et';
+const titleLevel = 2;
+
+const url = '/another-page';
+
+describe('ImageWidget', () => {
+ it('renders an image', () => {
+ render(
+ <ImageWidget
+ expanded={true}
+ image={img}
+ title={title}
+ level={titleLevel}
+ />
+ );
+ expect(screen.getByRole('img', { name: img.alt })).toBeInTheDocument();
+ });
+
+ it('renders a link', () => {
+ render(
+ <ImageWidget
+ expanded={true}
+ image={img}
+ title={title}
+ level={titleLevel}
+ url={url}
+ />
+ );
+ expect(screen.getByRole('link', { name: img.alt })).toHaveAttribute(
+ 'href',
+ url
+ );
+ });
+
+ it('renders a description', () => {
+ render(
+ <ImageWidget
+ expanded={true}
+ image={img}
+ description={description}
+ title={title}
+ level={titleLevel}
+ />
+ );
+ expect(screen.getByText(description)).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx
new file mode 100644
index 0000000..873337b
--- /dev/null
+++ b/src/components/organisms/widgets/image-widget.tsx
@@ -0,0 +1,69 @@
+import ResponsiveImage, {
+ type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
+import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
+import { FC } from 'react';
+import styles from './image-widget.module.scss';
+
+export type Alignment = 'left' | 'center' | 'right';
+
+export type Image = Pick<
+ ResponsiveImageProps,
+ 'alt' | 'height' | 'src' | 'width'
+>;
+
+export type ImageWidgetProps = Pick<
+ WidgetProps,
+ 'className' | 'expanded' | 'level' | 'title'
+> & {
+ /**
+ * The content alignment.
+ */
+ alignment?: Alignment;
+ /**
+ * Add a caption to the image.
+ */
+ description?: ResponsiveImageProps['caption'];
+ /**
+ * An object describing the image.
+ */
+ image: Image;
+ /**
+ * Set additional classnames to the image wrapper.
+ */
+ imageClassName?: string;
+ /**
+ * Add a link to the image.
+ */
+ url?: ResponsiveImageProps['target'];
+};
+
+/**
+ * ImageWidget component
+ *
+ * Renders a widget that print an image and an optional text.
+ */
+const ImageWidget: FC<ImageWidgetProps> = ({
+ alignment = 'left',
+ className = '',
+ description,
+ image,
+ imageClassName = '',
+ url,
+ ...props
+}) => {
+ const alignmentClass = `widget--${alignment}`;
+
+ return (
+ <Widget className={`${styles[alignmentClass]} ${className}`} {...props}>
+ <ResponsiveImage
+ target={url}
+ caption={description}
+ className={`${styles.figure} ${imageClassName}`}
+ {...image}
+ />
+ </Widget>
+ );
+};
+
+export default ImageWidget;
diff --git a/src/components/organisms/widgets/links-list-widget.module.scss b/src/components/organisms/widgets/links-list-widget.module.scss
new file mode 100644
index 0000000..4444df4
--- /dev/null
+++ b/src/components/organisms/widgets/links-list-widget.module.scss
@@ -0,0 +1,75 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/placeholders";
+
+.widget {
+ .list {
+ .list {
+ > *:first-child {
+ border-top: fun.convert-px(1) solid var(--color-primary);
+ }
+ }
+
+ &__link {
+ display: block;
+ padding: var(--spacing-2xs) var(--spacing-xs);
+ background: none;
+ text-decoration: underline solid transparent 0;
+
+ &:hover,
+ &:focus {
+ background: var(--color-bg-secondary);
+ font-weight: 600;
+ }
+
+ &:focus {
+ color: var(--color-primary);
+ text-decoration-color: var(--color-primary-light);
+ text-decoration-thickness: 0.25ex;
+ }
+
+ &:active {
+ background: var(--color-bg-tertiary);
+ text-decoration-color: transparent;
+ text-decoration-thickness: 0;
+ }
+ }
+
+ &--ordered {
+ @extend %reset-ordered-list;
+
+ counter-reset: link;
+
+ .list__link {
+ counter-increment: link;
+
+ &::before {
+ padding-right: var(--spacing-2xs);
+ content: counters(link, ".") ". ";
+ color: var(--color-secondary);
+ }
+ }
+ }
+
+ &--unordered {
+ @extend %reset-list;
+ }
+
+ &__item {
+ &:not(:last-child) {
+ border-bottom: fun.convert-px(1) solid var(--color-primary);
+ }
+
+ > .list {
+ .list__link {
+ padding-left: var(--spacing-md);
+ }
+
+ .list__item > .list {
+ .list__link {
+ padding-left: var(--spacing-xl);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/organisms/widgets/links-list-widget.stories.tsx b/src/components/organisms/widgets/links-list-widget.stories.tsx
new file mode 100644
index 0000000..cdfa96a
--- /dev/null
+++ b/src/components/organisms/widgets/links-list-widget.stories.tsx
@@ -0,0 +1,122 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import LinksListWidget from './links-list-widget';
+
+/**
+ * LinksListWidget - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Widgets/LinksList',
+ component: LinksListWidget,
+ args: {
+ kind: 'unordered',
+ },
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the list wrapper.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ items: {
+ description: 'The widget data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ kind: {
+ control: {
+ type: 'select',
+ },
+ description: 'The list kind: either ordered or unordered.',
+ options: ['ordered', 'unordered'],
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 'unordered' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ level: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The heading level.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The widget title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof LinksListWidget>;
+
+const Template: ComponentStory<typeof LinksListWidget> = (args) => (
+ <LinksListWidget {...args} />
+);
+
+const items = [
+ { name: 'Level 1: Item 1', url: '#' },
+ {
+ name: 'Level 1: Item 2',
+ url: '#',
+ child: [
+ { name: 'Level 2: Item 1', url: '#' },
+ { name: 'Level 2: Item 2', url: '#' },
+ {
+ name: 'Level 2: Item 3',
+ url: '#',
+ child: [
+ { name: 'Level 3: Item 1', url: '#' },
+ { name: 'Level 3: Item 2', url: '#' },
+ ],
+ },
+ { name: 'Level 2: Item 4', url: '#' },
+ ],
+ },
+ { name: 'Level 1: Item 3', url: '#' },
+ { name: 'Level 1: Item 4', url: '#' },
+];
+
+/**
+ * Links List Widget Stories - Unordered
+ */
+export const Unordered = Template.bind({});
+Unordered.args = {
+ items,
+ kind: 'unordered',
+ level: 2,
+ title: 'A list of links',
+};
+
+/**
+ * Links List Widget Stories - Ordered
+ */
+export const Ordered = Template.bind({});
+Ordered.args = {
+ items,
+ kind: 'ordered',
+ level: 2,
+ title: 'A list of links',
+};
diff --git a/src/components/organisms/widgets/links-list-widget.test.tsx b/src/components/organisms/widgets/links-list-widget.test.tsx
new file mode 100644
index 0000000..a8d6a35
--- /dev/null
+++ b/src/components/organisms/widgets/links-list-widget.test.tsx
@@ -0,0 +1,32 @@
+import { render, screen } from '@test-utils';
+import LinksListWidget from './links-list-widget';
+
+const title = 'Voluptatem minus autem';
+
+const items = [
+ { name: 'Item 1', url: '/item-1' },
+ { name: 'Item 2', url: '/item-2' },
+ { name: 'Item 3', url: '/item-3' },
+];
+
+describe('LinksListWidget', () => {
+ it('renders a widget title', () => {
+ render(<LinksListWidget items={items} title={title} level={2} />);
+ expect(
+ screen.getByRole('heading', { level: 2, name: new RegExp(title, 'i') })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the correct number of items', () => {
+ render(<LinksListWidget items={items} title={title} level={2} />);
+ expect(screen.getAllByRole('listitem')).toHaveLength(items.length);
+ });
+
+ it('renders some links', () => {
+ render(<LinksListWidget items={items} title={title} level={2} />);
+ expect(screen.getByRole('link', { name: items[0].name })).toHaveAttribute(
+ 'href',
+ items[0].url
+ );
+ });
+});
diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx
new file mode 100644
index 0000000..a9c677b
--- /dev/null
+++ b/src/components/organisms/widgets/links-list-widget.tsx
@@ -0,0 +1,85 @@
+import Link from '@components/atoms/links/link';
+import List, {
+ type ListProps,
+ type ListItem,
+} from '@components/atoms/lists/list';
+import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
+import { slugify } from '@utils/helpers/strings';
+import { FC } from 'react';
+import styles from './links-list-widget.module.scss';
+
+export type LinksListItems = {
+ /**
+ * An array of name/url couple child of this list item.
+ */
+ child?: LinksListItems[];
+ /**
+ * The item name.
+ */
+ name: string;
+ /**
+ * The item url.
+ */
+ url: string;
+};
+
+export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> &
+ Pick<ListProps, 'className' | 'kind'> & {
+ /**
+ * An array of name/url couple.
+ */
+ items: LinksListItems[];
+ };
+
+/**
+ * LinksListWidget component
+ *
+ * Render a list of links inside a widget.
+ */
+const LinksListWidget: FC<LinksListWidgetProps> = ({
+ className = '',
+ items,
+ kind = 'unordered',
+ ...props
+}) => {
+ const listKindClass = `list--${kind}`;
+
+ /**
+ * Format the widget data to be used as List items.
+ *
+ * @param {LinksListItems[]} data - The widget data.
+ * @returns {ListItem[]} The list items data.
+ */
+ const getListItems = (data: LinksListItems[]): ListItem[] => {
+ return data.map((item) => {
+ return {
+ id: slugify(item.name),
+ child: item.child && getListItems(item.child),
+ value: (
+ <Link href={item.url} className={styles.list__link}>
+ {item.name}
+ </Link>
+ ),
+ };
+ });
+ };
+
+ return (
+ <Widget
+ expanded={true}
+ withBorders={true}
+ className={styles.widget}
+ withScroll={true}
+ {...props}
+ >
+ <List
+ items={getListItems(items)}
+ kind={kind}
+ className={`${styles.list} ${styles[listKindClass]} ${className}`}
+ itemsClassName={styles.list__item}
+ />
+ </Widget>
+ );
+};
+
+export default LinksListWidget;
diff --git a/src/components/organisms/widgets/sharing.module.scss b/src/components/organisms/widgets/sharing.module.scss
new file mode 100644
index 0000000..e06d4e3
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.module.scss
@@ -0,0 +1,10 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.list {
+ display: flex;
+ flex-flow: row wrap;
+ gap: var(--spacing-xs);
+ margin: 0;
+ padding: 0 var(--spacing-2xs);
+ list-style-type: none;
+}
diff --git a/src/components/organisms/widgets/sharing.stories.tsx b/src/components/organisms/widgets/sharing.stories.tsx
new file mode 100644
index 0000000..59b86d3
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.stories.tsx
@@ -0,0 +1,91 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SharingWidget from './sharing';
+
+/**
+ * Sharing - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Widgets',
+ component: SharingWidget,
+ argTypes: {
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the sharing links list.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ data: {
+ description: 'The page data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ expanded: {
+ control: {
+ type: null,
+ },
+ description: 'Default widget state (expanded or collapsed).',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: true },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ level: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The heading level.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: 2 },
+ },
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ media: {
+ control: {
+ type: null,
+ },
+ description: 'An array of active and ordered sharing medium.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SharingWidget>;
+
+const Template: ComponentStory<typeof SharingWidget> = (args) => (
+ <SharingWidget {...args} />
+);
+
+/**
+ * Widgets Stories - Sharing
+ */
+export const Sharing = Template.bind({});
+Sharing.args = {
+ data: {
+ excerpt:
+ 'Alias similique eius ducimus laudantium aspernatur. Est rem ut eum temporibus sit reprehenderit aut non molestias. Vel dolorem expedita labore quo inventore aliquid nihil nam. Possimus nobis enim quas corporis eos.',
+ title: 'Accusantium totam nostrum',
+ url: 'https://www.example.test',
+ },
+ media: ['diaspora', 'facebook', 'linkedin', 'twitter', 'email'],
+};
diff --git a/src/components/organisms/widgets/sharing.test.tsx b/src/components/organisms/widgets/sharing.test.tsx
new file mode 100644
index 0000000..48da49e
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.test.tsx
@@ -0,0 +1,23 @@
+import { render, screen } from '@test-utils';
+import Sharing, { type SharingData } from './sharing';
+
+const postData: SharingData = {
+ excerpt: 'A post excerpt',
+ title: 'A post title',
+ url: 'https://sharing-website.test',
+};
+
+describe('Sharing', () => {
+ it('renders a sharing widget', () => {
+ render(<Sharing data={postData} media={['facebook', 'twitter']} />);
+ expect(
+ screen.getByRole('link', { name: 'Share on facebook' })
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('link', { name: 'Share on twitter' })
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByRole('link', { name: 'Share on linkedin' })
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx
new file mode 100644
index 0000000..c63f5db
--- /dev/null
+++ b/src/components/organisms/widgets/sharing.tsx
@@ -0,0 +1,214 @@
+import SharingLink, {
+ type SharingMedium,
+} from '@components/atoms/links/sharing-link';
+import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './sharing.module.scss';
+
+export type SharingData = {
+ /**
+ * The content excerpt.
+ */
+ excerpt: string;
+ /**
+ * The content title.
+ */
+ title: string;
+ /**
+ * The content url.
+ */
+ url: string;
+};
+
+export type SharingProps = {
+ /**
+ * Set additional classnames to the sharing links list.
+ */
+ className?: string;
+ /**
+ * The page data to share.
+ */
+ data: SharingData;
+ /**
+ * The widget default state.
+ */
+ expanded?: WidgetProps['expanded'];
+ /**
+ * The HTML heading level.
+ */
+ level?: WidgetProps['level'];
+ /**
+ * A list of active and ordered sharing medium.
+ */
+ media: SharingMedium[];
+};
+
+/**
+ * Sharing widget component
+ *
+ * Render a list of sharing links inside a widget.
+ */
+const Sharing: FC<SharingProps> = ({
+ className = '',
+ data,
+ media,
+ expanded = true,
+ level = 2,
+ ...props
+}) => {
+ const intl = useIntl();
+ const widgetTitle = intl.formatMessage({
+ defaultMessage: 'Share',
+ id: 'q3U6uI',
+ description: 'Sharing: widget title',
+ });
+
+ /**
+ * Build the Diaspora sharing url with provided data.
+ *
+ * @param {string} title - The content title.
+ * @param {string} url - The content url.
+ * @returns {string} The Diaspora url.
+ */
+ const buildDiasporaUrl = (title: string, url: string): string => {
+ const titleParam = `title=${encodeURI(title)}`;
+ const urlParam = `url=${encodeURI(url)}`;
+ return `https://share.diasporafoundation.org/?${titleParam}&${urlParam}`;
+ };
+
+ /**
+ * Build the mailto url from provided data.
+ *
+ * @param {string} excerpt - The content excerpt.
+ * @param {string} title - The content title.
+ * @param {string} url - The content url.
+ * @returns {string} The mailto url with params.
+ */
+ const buildEmailUrl = (
+ excerpt: string,
+ title: string,
+ url: string
+ ): string => {
+ const intro = intl.formatMessage({
+ defaultMessage: 'Introduction:',
+ description: 'Sharing: email content prefix',
+ id: 'yfgMcl',
+ });
+ const readMore = intl.formatMessage({
+ defaultMessage: 'Read more here:',
+ description: 'Sharing: content link prefix',
+ id: 'UsQske',
+ });
+ const body = `${intro}\n\n"${excerpt}"\n\n${readMore} ${url}`;
+ const bodyParam = excerpt ? `body=${encodeURI(body)}` : '';
+
+ const subject = intl.formatMessage(
+ {
+ defaultMessage: 'You should read {title}',
+ description: 'Sharing: subject text',
+ id: 'azgQuH',
+ },
+ { title }
+ );
+ const subjectParam = `subject=${encodeURI(subject)}`;
+
+ return `mailto:?${bodyParam}&${subjectParam}`;
+ };
+
+ /**
+ * Build the Facebook sharing url with provided data.
+ *
+ * @param {string} url - The content url.
+ * @returns {string} The Facebook url.
+ */
+ const buildFacebookUrl = (url: string): string => {
+ const urlParam = `u=${encodeURI(url)}`;
+ return `https://www.facebook.com/sharer/sharer.php?${urlParam}`;
+ };
+
+ /**
+ * Build the Journal du Hacker sharing url with provided data.
+ *
+ * @param {string} title - The content title.
+ * @param {string} url - The content url.
+ * @returns {string} The Journal du Hacker url.
+ */
+ const buildJdHUrl = (title: string, url: string): string => {
+ const titleParam = `title=${encodeURI(title)}`;
+ const urlParam = `url=${encodeURI(url)}`;
+ return `https://www.journalduhacker.net/stories/new?${titleParam}&${urlParam}`;
+ };
+
+ /**
+ * Build the LinkedIn sharing url with provided data.
+ *
+ * @param {string} url - The content url.
+ * @returns {string} The LinkedIn url.
+ */
+ const buildLinkedInUrl = (url: string): string => {
+ const urlParam = `url=${encodeURI(url)}`;
+ return `https://www.linkedin.com/sharing/share-offsite/?${urlParam}`;
+ };
+
+ /**
+ * Build the Twitter sharing url with provided data.
+ *
+ * @param {string} title - The content title.
+ * @param {string} url - The content url.
+ * @returns {string} The Twitter url.
+ */
+ const buildTwitterUrl = (title: string, url: string): string => {
+ const titleParam = `text=${encodeURI(title)}`;
+ const urlParam = `url=${encodeURI(url)}`;
+ return `https://twitter.com/intent/tweet?${titleParam}&${urlParam}`;
+ };
+
+ /**
+ * Retrieve the sharing url by medium id.
+ *
+ * @param {SharingMedium} medium - A sharing medium id.
+ * @returns {string} The sharing url.
+ */
+ const getUrl = (medium: SharingMedium): string => {
+ const { excerpt, title, url } = data;
+
+ switch (medium) {
+ case 'diaspora':
+ return buildDiasporaUrl(title, url);
+ case 'email':
+ return buildEmailUrl(excerpt, title, url);
+ case 'facebook':
+ return buildFacebookUrl(url);
+ case 'journal-du-hacker':
+ return buildJdHUrl(title, url);
+ case 'linkedin':
+ return buildLinkedInUrl(url);
+ case 'twitter':
+ return buildTwitterUrl(title, url);
+ default:
+ return '#';
+ }
+ };
+
+ /**
+ * Get the sharing list items.
+ *
+ * @returns {JSX.Element[]} The sharing links wrapped with li element.
+ */
+ const getItems = (): JSX.Element[] => {
+ return media.map((medium) => (
+ <li key={medium}>
+ <SharingLink medium={medium} url={getUrl(medium)} />
+ </li>
+ ));
+ };
+
+ return (
+ <Widget expanded={expanded} level={level} title={widgetTitle} {...props}>
+ <ul className={`${styles.list} ${className}`}>{getItems()}</ul>
+ </Widget>
+ );
+};
+
+export default Sharing;
diff --git a/src/components/organisms/widgets/social-media.module.scss b/src/components/organisms/widgets/social-media.module.scss
new file mode 100644
index 0000000..01b6c0e
--- /dev/null
+++ b/src/components/organisms/widgets/social-media.module.scss
@@ -0,0 +1,10 @@
+@use "@styles/abstracts/placeholders";
+
+.list {
+ @extend %reset-list;
+
+ display: flex;
+ flex-flow: row wrap;
+ gap: var(--spacing-xs);
+ padding: 0 var(--spacing-2xs);
+}
diff --git a/src/components/organisms/widgets/social-media.stories.tsx b/src/components/organisms/widgets/social-media.stories.tsx
new file mode 100644
index 0000000..6c9de4d
--- /dev/null
+++ b/src/components/organisms/widgets/social-media.stories.tsx
@@ -0,0 +1,61 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SocialMediaWidget, { Media } from './social-media';
+
+/**
+ * SocialMedia - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Widgets',
+ component: SocialMediaWidget,
+ argTypes: {
+ level: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ description: 'The heading level.',
+ type: {
+ name: 'number',
+ required: true,
+ },
+ },
+ media: {
+ description: 'The links data.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The widget title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof SocialMediaWidget>;
+
+const Template: ComponentStory<typeof SocialMediaWidget> = (args) => (
+ <SocialMediaWidget {...args} />
+);
+
+const media: Media[] = [
+ { name: 'Github', url: '#' },
+ { name: 'LinkedIn', url: '#' },
+];
+
+/**
+ * Widgets Stories - Social media
+ */
+export const SocialMedia = Template.bind({});
+SocialMedia.args = {
+ media,
+ title: 'Follow me',
+ level: 2,
+};
diff --git a/src/components/organisms/widgets/social-media.test.tsx b/src/components/organisms/widgets/social-media.test.tsx
new file mode 100644
index 0000000..e40db30
--- /dev/null
+++ b/src/components/organisms/widgets/social-media.test.tsx
@@ -0,0 +1,33 @@
+import { render, screen } from '@test-utils';
+import SocialMedia, { Media } from './social-media';
+
+const media: Media[] = [
+ { name: 'Github', url: '#' },
+ { name: 'LinkedIn', url: '#' },
+];
+const title = 'Dolores ut ut';
+const titleLevel = 2;
+
+/**
+ * Next.js mock images with next/image component. So for now, I need to mock
+ * the svg files manually.
+ */
+jest.mock('@assets/images/social-media/github.svg', () => 'svg-file');
+jest.mock('@assets/images/social-media/linkedin.svg', () => 'svg-file');
+
+describe('SocialMedia', () => {
+ it('renders the widget title', () => {
+ render(<SocialMedia media={media} title={title} level={titleLevel} />);
+ expect(
+ screen.getByRole('heading', {
+ level: titleLevel,
+ name: new RegExp(title, 'i'),
+ })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the correct number of items', () => {
+ render(<SocialMedia media={media} title={title} level={titleLevel} />);
+ expect(screen.getAllByRole('listitem')).toHaveLength(media.length);
+ });
+});
diff --git a/src/components/organisms/widgets/social-media.tsx b/src/components/organisms/widgets/social-media.tsx
new file mode 100644
index 0000000..58b2f73
--- /dev/null
+++ b/src/components/organisms/widgets/social-media.tsx
@@ -0,0 +1,41 @@
+import SocialLink, {
+ type SocialLinkProps,
+} from '@components/atoms/links/social-link';
+import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
+import { FC } from 'react';
+import styles from './social-media.module.scss';
+
+export type Media = SocialLinkProps;
+
+export type SocialMediaProps = Pick<WidgetProps, 'level' | 'title'> & {
+ media: Media[];
+};
+
+/**
+ * Social Media widget component
+ *
+ * Render a social media list with links.
+ */
+const SocialMedia: FC<SocialMediaProps> = ({ media, ...props }) => {
+ /**
+ * Retrieve the social media items.
+ *
+ * @param {SocialMedia[]} links - An array of social media name and url.
+ * @returns {JSX.Element[]} The social links.
+ */
+ const getItems = (links: Media[]): JSX.Element[] => {
+ return links.map((link, index) => (
+ <li key={`media-${index}`}>
+ <SocialLink name={link.name} url={link.url} />
+ </li>
+ ));
+ };
+
+ return (
+ <Widget expanded={true} {...props}>
+ <ul className={styles.list}>{getItems(media)}</ul>
+ </Widget>
+ );
+};
+
+export default SocialMedia;
diff --git a/src/components/organisms/widgets/table-of-contents.module.scss b/src/components/organisms/widgets/table-of-contents.module.scss
new file mode 100644
index 0000000..36217ed
--- /dev/null
+++ b/src/components/organisms/widgets/table-of-contents.module.scss
@@ -0,0 +1,4 @@
+.list {
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+}
diff --git a/src/components/organisms/widgets/table-of-contents.stories.tsx b/src/components/organisms/widgets/table-of-contents.stories.tsx
new file mode 100644
index 0000000..9490ee3
--- /dev/null
+++ b/src/components/organisms/widgets/table-of-contents.stories.tsx
@@ -0,0 +1,54 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ToCWidget from './table-of-contents';
+
+/**
+ * TableOfContents - Storybook Meta
+ */
+export default {
+ title: 'Organisms/Widgets',
+ component: ToCWidget,
+ argTypes: {
+ wrapper: {
+ control: {
+ type: null,
+ },
+ description:
+ 'A reference to the HTML element that contains the headings.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ },
+} as ComponentMeta<typeof ToCWidget>;
+
+const Template: ComponentStory<typeof ToCWidget> = (args) => (
+ <ToCWidget {...args} />
+);
+
+export const GetWrapper = () => {
+ const wrapper = document.createElement('div');
+ const firstTitle = document.createElement('h2');
+ const firstParagraph = document.createElement('p');
+ const secondTitle = document.createElement('h2');
+ const secondParagraph = document.createElement('p');
+
+ firstTitle.textContent = 'dignissimos odit odit';
+ firstParagraph.textContent =
+ 'Sint error saepe in. Vel doloribus facere deleniti minima magni. Consequatur veniam quia rerum praesentium eaque culpa culpa quas optio.';
+ secondTitle.textContent = 'aliquam exercitationem ut';
+ secondParagraph.textContent =
+ 'Doloribus sunt ut pariatur et praesentium rerum quam deserunt. Quod omnis quia qui quis debitis recusandae. Voluptate et impedit quam quidem quis id explicabo similique enim. Velit illum amet quos veniam consequatur amet nam sunt et. Et odit atque totam culpa officia saepe sed eaque consequatur.';
+
+ wrapper.append(...[firstTitle, firstParagraph, secondTitle, secondParagraph]);
+
+ return wrapper;
+};
+
+/**
+ * Widgets Stories - Table of Contents
+ */
+export const TableOfContents = Template.bind({});
+TableOfContents.args = {
+ wrapper: GetWrapper(),
+};
diff --git a/src/components/organisms/widgets/table-of-contents.test.tsx b/src/components/organisms/widgets/table-of-contents.test.tsx
new file mode 100644
index 0000000..2064f39
--- /dev/null
+++ b/src/components/organisms/widgets/table-of-contents.test.tsx
@@ -0,0 +1,12 @@
+import { render, screen } from '@test-utils';
+import TableOfContents from './table-of-contents';
+
+describe('TableOfContents', () => {
+ it('renders the ToC title', () => {
+ const divEl = document.createElement('div');
+ render(<TableOfContents wrapper={divEl} />);
+ expect(
+ screen.getByRole('heading', { level: 2, name: /Table of Contents/i })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/organisms/widgets/table-of-contents.tsx b/src/components/organisms/widgets/table-of-contents.tsx
new file mode 100644
index 0000000..800ff58
--- /dev/null
+++ b/src/components/organisms/widgets/table-of-contents.tsx
@@ -0,0 +1,55 @@
+import useHeadingsTree, { type Heading } from '@utils/hooks/use-headings-tree';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import LinksListWidget, { type LinksListItems } from './links-list-widget';
+import styles from './table-of-contents.module.scss';
+
+type TableOfContentsProps = {
+ /**
+ * A reference to the HTML element that contains the headings.
+ */
+ wrapper: HTMLElement;
+};
+
+/**
+ * Table of Contents widget component
+ *
+ * Render a table of contents.
+ */
+const TableOfContents: FC<TableOfContentsProps> = ({ wrapper }) => {
+ const intl = useIntl();
+ const headingsTree = useHeadingsTree(wrapper);
+ const title = intl.formatMessage({
+ defaultMessage: 'Table of Contents',
+ description: 'TableOfContents: the widget title',
+ id: 'WKG9wj',
+ });
+
+ /**
+ * Convert an headings tree to list items.
+ *
+ * @param {Heading[]} tree - The headings tree.
+ * @returns {LinksListItems[]} The list items.
+ */
+ const getItems = (tree: Heading[]): LinksListItems[] => {
+ return tree.map((heading) => {
+ return {
+ name: heading.title,
+ url: `#${heading.id}`,
+ child: getItems(heading.children),
+ };
+ });
+ };
+
+ return (
+ <LinksListWidget
+ kind="ordered"
+ title={title}
+ level={2}
+ items={getItems(headingsTree)}
+ className={styles.list}
+ />
+ );
+};
+
+export default TableOfContents;
diff --git a/src/components/templates/layout/layout.module.scss b/src/components/templates/layout/layout.module.scss
new file mode 100644
index 0000000..1080732
--- /dev/null
+++ b/src/components/templates/layout/layout.module.scss
@@ -0,0 +1,53 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.header {
+ border-bottom: fun.convert-px(3) solid var(--color-border-light);
+}
+
+.main {
+ flex: 1;
+}
+
+.article {
+ &--grid {
+ @extend %grid;
+
+ grid-auto-flow: column dense;
+ align-items: baseline;
+ }
+
+ &--padding {
+ padding-bottom: var(--spacing-lg);
+ }
+}
+
+.footer {
+ border-top: fun.convert-px(3) solid var(--color-border-light);
+}
+
+.back-to-top {
+ &--hidden {
+ opacity: 0;
+ transform: translateY(calc(var(--button-size) + var(--spacing-md)));
+ visibility: hidden;
+ }
+
+ &--visible {
+ opacity: 1;
+ transform: translateY(0);
+ visibility: visible;
+ }
+}
+
+.noscript-spacing {
+ width: 100%;
+ height: fun.convert-px(75);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("xs") {
+ height: fun.convert-px(50);
+ }
+ }
+}
diff --git a/src/components/templates/layout/layout.stories.tsx b/src/components/templates/layout/layout.stories.tsx
new file mode 100644
index 0000000..4666b07
--- /dev/null
+++ b/src/components/templates/layout/layout.stories.tsx
@@ -0,0 +1,117 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import LayoutComponent from './layout';
+
+/**
+ * Layout - Storybook Meta
+ */
+export default {
+ title: 'Templates/LayoutBase',
+ component: LayoutComponent,
+ args: {
+ breadcrumbSchema: [],
+ isHome: false,
+ },
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The article content.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ breadcrumbSchema: {
+ control: {
+ type: 'null',
+ },
+ description: 'The JSON schema for breadcrumb items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ isHome: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if it is the homepage.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the article element.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ useGrid: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Use the grid layout.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ withExtraPadding: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Set additional padding at the bottom of the page.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+ <div
+ id="__next"
+ style={{
+ flex: 1,
+ display: 'flex',
+ flexFlow: 'column nowrap',
+ minHeight: '100vh',
+ }}
+ >
+ <Story />
+ </div>
+ ),
+ ],
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof LayoutComponent>;
+
+const Template: ComponentStory<typeof LayoutComponent> = (args) => (
+ <LayoutComponent {...args} />
+);
+
+/**
+ * Layout Stories - Default
+ */
+export const LayoutBase = Template.bind({});
diff --git a/src/components/templates/layout/layout.test.tsx b/src/components/templates/layout/layout.test.tsx
new file mode 100644
index 0000000..78547d4
--- /dev/null
+++ b/src/components/templates/layout/layout.test.tsx
@@ -0,0 +1,35 @@
+import { render, screen } from '@test-utils';
+import { BreadcrumbList } from 'schema-dts';
+import Layout from './layout';
+
+const body =
+ 'Sit dolorem eveniet. Sit sit odio nemo vitae corrupti modi sint est rerum. Pariatur quidem maiores distinctio. Quia et illum aspernatur est cum.';
+
+describe('Layout', () => {
+ it('renders the website header', () => {
+ render(<Layout>{body}</Layout>);
+ expect(screen.getByRole('banner')).toBeInTheDocument();
+ });
+
+ it('renders the website main content', () => {
+ render(<Layout>{body}</Layout>);
+ expect(screen.getByRole('main')).toBeInTheDocument();
+ });
+
+ it('renders the website footer', () => {
+ render(<Layout>{body}</Layout>);
+ expect(screen.getByRole('contentinfo')).toBeInTheDocument();
+ });
+
+ it('renders a skip to content link', () => {
+ render(<Layout>{body}</Layout>);
+ expect(
+ screen.getByRole('link', { name: 'Skip to content' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders an article', () => {
+ render(<Layout>{body}</Layout>);
+ expect(screen.getByRole('article')).toHaveTextContent(body);
+ });
+});
diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx
new file mode 100644
index 0000000..8f0d4e7
--- /dev/null
+++ b/src/components/templates/layout/layout.tsx
@@ -0,0 +1,242 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Career from '@components/atoms/icons/career';
+import CCBySA from '@components/atoms/icons/cc-by-sa';
+import ComputerScreen from '@components/atoms/icons/computer-screen';
+import Envelop from '@components/atoms/icons/envelop';
+import Home from '@components/atoms/icons/home';
+import PostsStack from '@components/atoms/icons/posts-stack';
+import Main from '@components/atoms/layout/main';
+import NoScript from '@components/atoms/layout/no-script';
+import Footer, { type FooterProps } from '@components/organisms/layout/footer';
+import Header, { type HeaderProps } from '@components/organisms/layout/header';
+import { type NextPageWithLayoutOptions } from '@ts/types/app';
+import useScrollPosition from '@utils/hooks/use-scroll-position';
+import useSettings from '@utils/hooks/use-settings';
+import Script from 'next/script';
+import { FC, ReactElement, ReactNode, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { Person, SearchAction, WebSite, WithContext } from 'schema-dts';
+import styles from './layout.module.scss';
+
+export type QueryAction = SearchAction & {
+ 'query-input': string;
+};
+
+export type LayoutProps = Pick<HeaderProps, 'isHome'> & {
+ /**
+ * The layout main content.
+ */
+ children: ReactNode;
+ /**
+ * Determine if article has a comments section.
+ */
+ withExtraPadding?: boolean;
+ /**
+ * Determine if article should use grid. Default: false.
+ */
+ useGrid?: boolean;
+};
+
+/**
+ * Layout component
+ *
+ * Render the base layout used by all pages.
+ */
+const Layout: FC<LayoutProps> = ({
+ children,
+ withExtraPadding = false,
+ isHome,
+ useGrid = false,
+}) => {
+ const intl = useIntl();
+ const { website } = useSettings();
+ const { baseline, copyright, locales, name, picture, url } = website;
+ const articleGridClass = useGrid ? 'article--grid' : '';
+ const articleCommentsClass = withExtraPadding ? 'article--padding' : '';
+
+ const skipToContent = intl.formatMessage({
+ defaultMessage: 'Skip to content',
+ description: 'Layout: Skip to content link',
+ id: 'K4rYdT',
+ });
+ const noScript = intl.formatMessage({
+ defaultMessage:
+ 'Warning: If you want to benefit from all features (search for example), please activate Javascript.',
+ description: 'Layout: noscript message',
+ id: '7jVUT6',
+ });
+
+ const copyrightData = {
+ dates: {
+ start: copyright.start,
+ end: copyright.end,
+ },
+ owner: name,
+ icon: <CCBySA />,
+ };
+
+ const homeLabel = intl.formatMessage({
+ defaultMessage: 'Home',
+ description: 'Layout: main nav - home link',
+ id: 'bojYF5',
+ });
+ const blogLabel = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'Layout: main nav - blog link',
+ id: 'D8vB38',
+ });
+ const projectsLabel = intl.formatMessage({
+ defaultMessage: 'Projects',
+ description: 'Layout: main nav - projects link',
+ id: 'qnwsWV',
+ });
+ const cvLabel = intl.formatMessage({
+ defaultMessage: 'CV',
+ description: 'Layout: main nav - cv link',
+ id: 'R895yC',
+ });
+ const contactLabel = intl.formatMessage({
+ defaultMessage: 'Contact',
+ description: 'Layout: main nav - contact link',
+ id: 'AE4kCD',
+ });
+
+ const mainNav: HeaderProps['nav'] = [
+ { id: 'home', label: homeLabel, href: '/', logo: <Home /> },
+ { id: 'blog', label: blogLabel, href: '/blog', logo: <PostsStack /> },
+ {
+ id: 'projects',
+ label: projectsLabel,
+ href: '/projets',
+ logo: <ComputerScreen />,
+ },
+ { id: 'cv', label: cvLabel, href: '/cv', logo: <Career /> },
+ { id: 'contact', label: contactLabel, href: '/contact', logo: <Envelop /> },
+ ];
+
+ const legalNoticeLabel = intl.formatMessage({
+ defaultMessage: 'Legal notice',
+ description: 'Layout: Legal notice label',
+ id: 'nwbzKm',
+ });
+
+ const footerNav: FooterProps['navItems'] = [
+ { id: 'legal-notice', label: legalNoticeLabel, href: '/mentions-legales' },
+ ];
+
+ const searchActionSchema: QueryAction = {
+ '@type': 'SearchAction',
+ target: {
+ '@type': 'EntryPoint',
+ urlTemplate: `${url}/recherche?s={search_term_string}`,
+ },
+ query: 'required',
+ 'query-input': 'required name=search_term_string',
+ };
+
+ const schemaJsonLd: WithContext<WebSite> = {
+ '@context': 'https://schema.org',
+ '@id': `${url}`,
+ '@type': 'WebSite',
+ name: name,
+ description: baseline,
+ url: url,
+ author: { '@id': `${url}/#branding` },
+ copyrightYear: Number(copyright.start),
+ creator: { '@id': `${url}/#branding` },
+ editor: { '@id': `${url}/#branding` },
+ inLanguage: locales.default,
+ potentialAction: searchActionSchema,
+ };
+
+ const brandingSchema: WithContext<Person> = {
+ '@context': 'https://schema.org',
+ '@type': 'Person',
+ '@id': `${url}/#branding`,
+ name: name,
+ url: url,
+ jobTitle: baseline,
+ image: picture.src,
+ subjectOf: { '@id': `${url}` },
+ };
+
+ const [backToTopClassName, setBackToTopClassName] = useState<string>(
+ styles['back-to-top--hidden']
+ );
+ const updateBackToTopClassName = () => {
+ setBackToTopClassName(
+ window.scrollY > 300
+ ? styles['back-to-top--visible']
+ : styles['back-to-top--hidden']
+ );
+ };
+
+ useScrollPosition(updateBackToTopClassName);
+
+ return (
+ <>
+ <Script
+ id="schema-layout"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <Script
+ id="schema-branding"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }}
+ />
+ <noscript>
+ <div className={styles['noscript-spacing']}></div>
+ </noscript>
+ <span tabIndex={-1}></span>
+ <ButtonLink target="#main" className="screen-reader-text">
+ {skipToContent}
+ </ButtonLink>
+ <Header
+ ackeeStorageKey="ackee-tracking"
+ baseline={baseline}
+ className={styles.header}
+ isHome={isHome}
+ motionStorageKey="reduced-motion"
+ nav={mainNav}
+ photo={picture}
+ searchPage="/recherche"
+ title={name}
+ withLink={true}
+ />
+ <Main id="main" className={styles.main}>
+ <article
+ className={`${styles[articleGridClass]} ${styles[articleCommentsClass]}`}
+ >
+ {children}
+ </article>
+ </Main>
+ <Footer
+ copyright={copyrightData}
+ navItems={footerNav}
+ topId="top"
+ backToTopClassName={backToTopClassName}
+ className={styles.footer}
+ />
+ <noscript>
+ <NoScript message={noScript} position="top" />
+ </noscript>
+ </>
+ );
+};
+
+/**
+ * Get the global layout.
+ *
+ * @param {ReactElement} page - A page.
+ * @param {boolean} [isHome] - Determine if it is the homepage.
+ * @returns A page wrapped with the global layout.
+ */
+export const getLayout = (
+ page: ReactElement,
+ props: NextPageWithLayoutOptions
+) => {
+ return <Layout {...props}>{page}</Layout>;
+};
+
+export default Layout;
diff --git a/src/components/templates/page/page-layout.module.scss b/src/components/templates/page/page-layout.module.scss
new file mode 100644
index 0000000..c7674ae
--- /dev/null
+++ b/src/components/templates/page/page-layout.module.scss
@@ -0,0 +1,92 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+.breadcrumb {
+ @extend %grid;
+
+ grid-column: 1 / -1;
+ padding: var(--spacing-md) 0;
+
+ > * {
+ grid-column: 2;
+ }
+
+ &__items {
+ font-size: var(--font-size-sm);
+ }
+}
+
+.header {
+ grid-column: 1 / -1;
+ margin-bottom: var(--spacing-md);
+}
+
+.body {
+ grid-column: 2;
+
+ > * + * {
+ margin-top: var(--spacing-sm);
+ margin-bottom: var(--spacing-sm);
+ }
+}
+
+.sidebar {
+ grid-column: 2;
+
+ &--first {
+ margin-bottom: var(--spacing-xs);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("lg") {
+ grid-column: 1;
+ align-self: stretch;
+ margin: 0 var(--spacing-xs) var(--spacing-md);
+ }
+ }
+ }
+
+ &--last {
+ margin: var(--spacing-lg) 0 0;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ grid-column: 3;
+ align-self: stretch;
+ margin: 0 var(--spacing-xs) var(--spacing-md);
+ }
+ }
+ }
+}
+
+.footer {
+ grid-column: 2;
+ margin: var(--spacing-sm) 0 var(--spacing-2xs);
+}
+
+.comments {
+ @extend %grid;
+
+ grid-column: 1 / -1;
+ margin: var(--spacing-lg) 0 0;
+ padding: 0 0 var(--spacing-lg);
+ background: var(--color-bg-secondary);
+ border-top: fun.convert-px(3) solid var(--color-border-light);
+
+ &__section {
+ grid-column: 2;
+
+ &:first-child {
+ margin: var(--spacing-md) 0 0;
+ }
+ }
+
+ &__no-comments {
+ text-align: center;
+ }
+
+ &__form {
+ max-width: 40ch;
+ margin: auto;
+ }
+}
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
new file mode 100644
index 0000000..06c6c24
--- /dev/null
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -0,0 +1,387 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Heading from '@components/atoms/headings/heading';
+import Link from '@components/atoms/links/link';
+import { comments } from '@components/organisms/layout/comments-list.fixture';
+import PostsList from '@components/organisms/layout/posts-list';
+import { posts } from '@components/organisms/layout/posts-list.fixture';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import Sharing from '@components/organisms/widgets/sharing';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { LayoutBase } from '../layout/layout.stories';
+import PageLayoutComponent from './page-layout';
+
+/**
+ * PageLayout - Storybook Meta
+ */
+export default {
+ title: 'Templates/Page',
+ component: PageLayoutComponent,
+ args: {
+ allowComments: false,
+ breadcrumbSchema: [],
+ },
+ argTypes: {
+ allowComments: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the comment form is displayed.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ bodyAttributes: {
+ description: 'Set additional HTML attributes to the main content body.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ bodyClassName: {
+ control: {
+ type: 'text',
+ },
+ description: 'Set additional classnames to the main content body.',
+ table: {
+ category: 'Styles',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ breadcrumb: {
+ description: 'The breadcrumb items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ breadcrumbSchema: {
+ control: {
+ type: null,
+ },
+ description: 'The JSON schema for breadcrumb items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page content.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ comments: {
+ description: 'The page comments.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ footerMeta: {
+ description: 'The metadata to display in the page footer.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ headerMeta: {
+ description: 'The metadata to display in the page header.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ id: {
+ control: {
+ type: 'number',
+ },
+ description: 'The page id.',
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
+ intro: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page introduction.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ title: {
+ control: {
+ type: 'text',
+ },
+ description: 'The page title.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ widgets: {
+ control: {
+ type: null,
+ },
+ description: 'An array of widgets to put inside the last sidebar.',
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ withToC: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Determine if the Table of Contents should be in the page.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ },
+ decorators: [
+ (Story, context) => (
+ <LayoutBase
+ useGrid={true}
+ withExtraPadding={!context.args.allowComments && !context.args.comments}
+ {...LayoutBase.args}
+ >
+ <Story />
+ </LayoutBase>
+ ),
+ ],
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof PageLayoutComponent>;
+
+const Template: ComponentStory<typeof PageLayoutComponent> = (args) => (
+ <PageLayoutComponent {...args} />
+);
+
+const pageTitle = 'Incidunt ad earum';
+const pageIntro =
+ 'Recusandae mollitia enim quo omnis rerum enim corporis ratione quidem. Pariatur omnis quas est ut ut numquam totam. Sunt sapiente nostrum aut sunt provident perspiciatis magni illum. Quidem nihil velit quasi fugit minima sint.';
+const pageBreadcrumb = [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'page', url: '#', name: pageTitle },
+];
+
+/**
+ * Page Layout Stories - Single Page
+ */
+export const SinglePage = Template.bind({});
+SinglePage.args = {
+ breadcrumb: pageBreadcrumb,
+ title: pageTitle,
+ intro: pageIntro,
+ children: (
+ <>
+ <Heading level={2}>Impedit commodi rerum</Heading>
+ <p>
+ Omnis vel earum cupiditate delectus reprehenderit perferendis distinctio
+ omnis. Laudantium rem tempore eligendi porro officia est dolorum
+ assumenda. Corrupti tempore quia ab. Quidem est inventore. Autem
+ nesciunt sed rerum praesentium.
+ </p>
+ <p>
+ Illo nostrum inventore tenetur quo repellendus autem nisi nostrum
+ dolore. Et velit assumenda. Veniam harum officia et. Blanditiis et et
+ qui cum. Rerum illum quo doloribus neque non velit. Unde iusto et eaque
+ a ut.
+ </p>
+ <Heading level={2}>Et omnis ducimus</Heading>
+ <p>
+ Dolor quidem quas perferendis in nam molestiae. Accusamus quidem
+ accusantium quaerat est praesentium accusamus ab dolorem. Beatae illum
+ totam et corrupti assumenda corporis aut illo animi.
+ </p>
+ <p>
+ Ad rem soluta. Est tenetur consequatur sequi voluptates autem. Molestiae
+ in neque dignissimos. Dolorum numquam quos quam voluptas atque facilis
+ et. Accusantium fuga architecto excepturi consequatur libero est.
+ </p>
+ </>
+ ),
+ widgets: [
+ <Sharing
+ key="sidebar2-widget1"
+ data={{ excerpt: pageIntro, title: pageTitle, url: '#' }}
+ media={[
+ 'diaspora',
+ 'email',
+ 'facebook',
+ 'journal-du-hacker',
+ 'linkedin',
+ 'twitter',
+ ]}
+ level={2}
+ expanded={true}
+ />,
+ ],
+ withToC: true,
+};
+
+const postBreadcrumb = [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'blog', url: '#', name: 'Blog' },
+ { id: 'post', url: '#', name: pageTitle },
+];
+
+/**
+ * Page Layout Stories - Post
+ */
+export const Post = Template.bind({});
+Post.args = {
+ breadcrumb: postBreadcrumb,
+ title: pageTitle,
+ intro: pageIntro,
+ headerMeta: {
+ publication: { date: '2020-03-14' },
+ thematics: [
+ <Link key="cat1" href="#">
+ Cat 1
+ </Link>,
+ <Link key="cat2" href="#">
+ Cat 2
+ </Link>,
+ ],
+ },
+ footerMeta: {
+ custom: {
+ label: 'Read more about:',
+ value: <ButtonLink target="#">Topic 1</ButtonLink>,
+ },
+ },
+ children: (
+ <>
+ <Heading level={2}>Impedit commodi rerum</Heading>
+ <p>
+ Omnis vel earum cupiditate delectus reprehenderit perferendis distinctio
+ omnis. Laudantium rem tempore eligendi porro officia est dolorum
+ assumenda. Corrupti tempore quia ab. Quidem est inventore. Autem
+ nesciunt sed rerum praesentium.
+ </p>
+ <p>
+ Illo nostrum inventore tenetur quo repellendus autem nisi nostrum
+ dolore. Et velit assumenda. Veniam harum officia et. Blanditiis et et
+ qui cum. Rerum illum quo doloribus neque non velit. Unde iusto et eaque
+ a ut.
+ </p>
+ <Heading level={2}>Et omnis ducimus</Heading>
+ <p>
+ Dolor quidem quas perferendis in nam molestiae. Accusamus quidem
+ accusantium quaerat est praesentium accusamus ab dolorem. Beatae illum
+ totam et corrupti assumenda corporis aut illo animi.
+ </p>
+ <p>
+ Ad rem soluta. Est tenetur consequatur sequi voluptates autem. Molestiae
+ in neque dignissimos. Dolorum numquam quos quam voluptas atque facilis
+ et. Accusantium fuga architecto excepturi consequatur libero est.
+ </p>
+ </>
+ ),
+ widgets: [
+ <Sharing
+ key="sidebar2-widget1"
+ data={{ excerpt: pageIntro, title: pageTitle, url: '#' }}
+ media={[
+ 'diaspora',
+ 'email',
+ 'facebook',
+ 'journal-du-hacker',
+ 'linkedin',
+ 'twitter',
+ ]}
+ level={2}
+ expanded={true}
+ />,
+ ],
+ withToC: true,
+ comments: comments,
+ allowComments: true,
+};
+
+const postsListBreadcrumb = [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'blog', url: '#', name: 'Blog' },
+];
+
+const blogCategories = [
+ { name: 'Cat 1', url: '#' },
+ {
+ name: 'Cat 2',
+ url: '#',
+ },
+ { name: 'Cat 3', url: '#' },
+ { name: 'Cat 4', url: '#' },
+];
+
+/**
+ * Page Layout Stories - Posts list
+ */
+export const Blog = Template.bind({});
+Blog.args = {
+ breadcrumb: postsListBreadcrumb,
+ title: 'Blog',
+ headerMeta: { total: posts.length },
+ children: (
+ <>
+ <PostsList
+ posts={posts}
+ byYear={true}
+ total={posts.length}
+ searchPage="#"
+ />
+ </>
+ ),
+ widgets: [
+ <LinksListWidget
+ key="sidebar-widget1"
+ items={blogCategories}
+ title="Categories"
+ level={2}
+ />,
+ ],
+};
diff --git a/src/components/templates/page/page-layout.test.tsx b/src/components/templates/page/page-layout.test.tsx
new file mode 100644
index 0000000..f2d07d7
--- /dev/null
+++ b/src/components/templates/page/page-layout.test.tsx
@@ -0,0 +1,107 @@
+import { comments } from '@components/organisms/layout/comments-list.fixture';
+import { render, screen } from '@test-utils';
+import { BreadcrumbList } from 'schema-dts';
+import PageLayout from './page-layout';
+
+const title = 'Incidunt ad earum';
+const breadcrumb = [
+ { id: 'home', url: '#', name: 'Home' },
+ { id: 'page', url: '#', name: title },
+];
+const breadcrumbSchema: BreadcrumbList['itemListElement'][] = [];
+const children =
+ 'Reprehenderit aut quis aperiam magnam quia id. Vero enim animi placeat quia. Laborum sit odio minima. Dolores et debitis eaque iste quidem. Omnis aliquam illum porro ea non. Quaerat totam iste quos ex facilis officia accusantium.';
+
+describe('PageLayout', () => {
+ it('renders the page title', () => {
+ render(
+ <PageLayout
+ breadcrumb={breadcrumb}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ >
+ {children}
+ </PageLayout>
+ );
+ expect(
+ screen.getByRole('heading', { level: 1, name: title })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the page content', () => {
+ render(
+ <PageLayout
+ breadcrumb={breadcrumb}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ >
+ {children}
+ </PageLayout>
+ );
+ expect(screen.getByText(children)).toBeInTheDocument();
+ });
+
+ it('renders the breadcrumb', () => {
+ render(
+ <PageLayout
+ breadcrumb={breadcrumb}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ >
+ {children}
+ </PageLayout>
+ );
+ expect(
+ screen.getByRole('navigation', { name: 'Breadcrumb' })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the table of contents', () => {
+ render(
+ <PageLayout
+ breadcrumb={breadcrumb}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ withToC={true}
+ >
+ {children}
+ </PageLayout>
+ );
+ expect(
+ screen.getByRole('heading', { level: 2, name: /Table of Contents/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the comment form', () => {
+ render(
+ <PageLayout
+ breadcrumb={breadcrumb}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ allowComments={true}
+ >
+ {children}
+ </PageLayout>
+ );
+ expect(
+ screen.getByRole('form', { name: /Leave a comment/i })
+ ).toBeInTheDocument();
+ });
+
+ it('renders the comments list', () => {
+ render(
+ <PageLayout
+ breadcrumb={breadcrumb}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ allowComments={true}
+ comments={comments}
+ >
+ {children}
+ </PageLayout>
+ );
+ expect(
+ screen.getByRole('heading', { level: 2, name: /Comments/i })
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
new file mode 100644
index 0000000..f96666e
--- /dev/null
+++ b/src/components/templates/page/page-layout.tsx
@@ -0,0 +1,297 @@
+import Heading from '@components/atoms/headings/heading';
+import Notice, { type NoticeKind } from '@components/atoms/layout/notice';
+import Sidebar from '@components/atoms/layout/sidebar';
+import { MetaData } from '@components/molecules/layout/meta';
+import PageFooter, {
+ type PageFooterProps,
+} from '@components/molecules/layout/page-footer';
+import PageHeader, {
+ type PageHeaderProps,
+} from '@components/molecules/layout/page-header';
+import Breadcrumb, {
+ type BreadcrumbItem,
+} from '@components/molecules/nav/breadcrumb';
+import CommentForm, {
+ type CommentFormProps,
+} from '@components/organisms/forms/comment-form';
+import CommentsList, {
+ type CommentsListProps,
+} from '@components/organisms/layout/comments-list';
+import TableOfContents from '@components/organisms/widgets/table-of-contents';
+import { type SendCommentVars } from '@services/graphql/api';
+import { sendComment } from '@services/graphql/comments';
+import useIsMounted from '@utils/hooks/use-is-mounted';
+import Script from 'next/script';
+import { FC, HTMLAttributes, ReactNode, useRef, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { BreadcrumbList } from 'schema-dts';
+import styles from './page-layout.module.scss';
+
+export type PageLayoutProps = {
+ /**
+ * True if the page accepts new comments. Default: false.
+ */
+ allowComments?: boolean;
+ /**
+ * Set attributes to the page body.
+ */
+ bodyAttributes?: HTMLAttributes<HTMLDivElement>;
+ /**
+ * Set additional classnames to the body wrapper.
+ */
+ bodyClassName?: string;
+ /**
+ * The breadcrumb items.
+ */
+ breadcrumb: BreadcrumbItem[];
+ /**
+ * The breadcrumb JSON schema.
+ */
+ breadcrumbSchema: BreadcrumbList['itemListElement'][];
+ /**
+ * The main content of the page.
+ */
+ children: ReactNode;
+ /**
+ * The page comments
+ */
+ comments?: CommentsListProps['comments'];
+ /**
+ * The footer metadata.
+ */
+ footerMeta?: PageFooterProps['meta'];
+ /**
+ * The header metadata.
+ */
+ headerMeta?: PageHeaderProps['meta'];
+ /**
+ * The page id.
+ */
+ id?: number;
+ /**
+ * The page introduction.
+ */
+ intro?: PageHeaderProps['intro'];
+ /**
+ * The page title.
+ */
+ title: PageHeaderProps['title'];
+ /**
+ * An array of widgets to put in the last sidebar.
+ */
+ widgets?: ReactNode[];
+ /**
+ * Show the table of contents. Default: false.
+ */
+ withToC?: boolean;
+};
+
+/**
+ * PageLayout component
+ *
+ * Render the pages layout.
+ */
+const PageLayout: FC<PageLayoutProps> = ({
+ children,
+ allowComments = false,
+ bodyAttributes,
+ bodyClassName = '',
+ breadcrumb,
+ breadcrumbSchema,
+ comments,
+ footerMeta,
+ headerMeta,
+ id,
+ intro,
+ title,
+ widgets,
+ withToC = false,
+}) => {
+ const intl = useIntl();
+ const commentsTitle = intl.formatMessage({
+ defaultMessage: 'Comments',
+ description: 'PageLayout: comments title',
+ id: '+dJU3e',
+ });
+ const commentFormTitle = intl.formatMessage({
+ defaultMessage: 'Leave a comment',
+ description: 'PageLayout: comment form title',
+ id: 'kzIYoQ',
+ });
+
+ const bodyRef = useRef<HTMLDivElement>(null);
+ const isMounted = useIsMounted(bodyRef);
+ const hasComments = Array.isArray(comments) && comments.length > 0;
+ const [status, setStatus] = useState<NoticeKind>('info');
+ const [statusMessage, setStatusMessage] = useState<string>('');
+ const isReplyRef = useRef<boolean>(false);
+
+ const saveComment: CommentFormProps['saveComment'] = async (data, reset) => {
+ if (!id) throw new Error('Page id missing. Cannot save comment.');
+
+ const { comment: commentBody, email, name, parentId, website } = data;
+ const commentData: SendCommentVars = {
+ author: name,
+ authorEmail: email,
+ authorUrl: website || '',
+ clientMutationId: 'contact',
+ commentOn: id,
+ content: commentBody,
+ parent: parentId,
+ };
+ const { comment, success } = await sendComment(commentData);
+
+ isReplyRef.current = !!parentId;
+
+ if (success) {
+ setStatus('success');
+ const successPrefix = intl.formatMessage({
+ defaultMessage: 'Thanks, your comment was successfully sent.',
+ description: 'PageLayout: comment form success message',
+ id: 'B290Ph',
+ });
+ const successMessage = comment?.approved
+ ? intl.formatMessage({
+ defaultMessage: 'It has been approved.',
+ id: 'g3+Ahv',
+ description: 'PageLayout: comment approved.',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'It is now awaiting moderation.',
+ id: 'Vmj5cw',
+ description: 'PageLayout: comment awaiting moderation',
+ });
+ setStatusMessage(`${successPrefix} ${successMessage}`);
+ reset();
+ } else {
+ const error = intl.formatMessage({
+ defaultMessage: 'An error occurred:',
+ description: 'PageLayout: comment form error message',
+ id: 'fkcTGp',
+ });
+ setStatus('error');
+ setStatusMessage(error);
+ }
+ };
+
+ /**
+ * Check if meta properties are defined.
+ *
+ * @param {MetaData} meta - The metadata.
+ */
+ const hasMeta = (meta: MetaData) => {
+ return Object.values(meta).every((value) => value);
+ };
+
+ return (
+ <>
+ <Script
+ id="schema-breadcrumb"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+ />
+ <Breadcrumb
+ items={breadcrumb}
+ className={styles.breadcrumb}
+ itemClassName={styles.breadcrumb__items}
+ />
+ <PageHeader
+ title={title}
+ intro={intro}
+ meta={headerMeta}
+ className={styles.header}
+ />
+ {withToC && (
+ <Sidebar
+ className={`${styles.sidebar} ${styles['sidebar--first']}`}
+ aria-label={intl.formatMessage({
+ defaultMessage: 'Table of contents sidebar',
+ id: 'Q+1GbT',
+ description: 'PageLayout: accessible name for ToC sidebar',
+ })}
+ >
+ {isMounted && bodyRef.current && (
+ <TableOfContents wrapper={bodyRef.current} />
+ )}
+ </Sidebar>
+ )}
+ {typeof children === 'string' ? (
+ <div
+ ref={bodyRef}
+ className={`${styles.body} ${bodyClassName}`}
+ dangerouslySetInnerHTML={{ __html: children }}
+ {...bodyAttributes}
+ />
+ ) : (
+ <div ref={bodyRef} className={`${styles.body} ${bodyClassName}`}>
+ {children}
+ </div>
+ )}
+ {footerMeta && hasMeta(footerMeta) && (
+ <PageFooter meta={footerMeta} className={styles.footer} />
+ )}
+ <Sidebar
+ className={`${styles.sidebar} ${styles['sidebar--last']}`}
+ aria-label={intl.formatMessage({
+ defaultMessage: 'Sidebar',
+ id: 'c556Qo',
+ description: 'PageLayout: accessible name for the sidebar',
+ })}
+ >
+ {widgets}
+ </Sidebar>
+ {allowComments && (
+ <div className={styles.comments} id="comments">
+ <section className={styles.comments__section}>
+ <Heading level={2} alignment="center">
+ {commentsTitle}
+ </Heading>
+ {hasComments ? (
+ <CommentsList
+ comments={comments}
+ depth={2}
+ Notice={
+ isReplyRef.current === true && (
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ )
+ }
+ saveComment={saveComment}
+ />
+ ) : (
+ <p className={styles['comments__no-comments']}>
+ {intl.formatMessage({
+ defaultMessage: 'No comments.',
+ id: 'sBwfCy',
+ description: 'PageLayout: no comments text',
+ })}
+ </p>
+ )}
+ </section>
+ <section className={styles.comments__section}>
+ <CommentForm
+ className={styles.comments__form}
+ saveComment={saveComment}
+ title={commentFormTitle}
+ titleAlignment="center"
+ Notice={
+ isReplyRef.current === false && (
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ )
+ }
+ />
+ </section>
+ </div>
+ )}
+ </>
+ );
+};
+
+export default PageLayout;
diff --git a/src/components/templates/sectioned/sectioned-layout.stories.tsx b/src/components/templates/sectioned/sectioned-layout.stories.tsx
new file mode 100644
index 0000000..689f9a7
--- /dev/null
+++ b/src/components/templates/sectioned/sectioned-layout.stories.tsx
@@ -0,0 +1,80 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { LayoutBase } from '../layout/layout.stories';
+import SectionedLayoutComponent from './sectioned-layout';
+
+/**
+ * SectionedLayout - Storybook Meta
+ */
+export default {
+ title: 'Templates/Sectioned',
+ component: SectionedLayoutComponent,
+ args: {
+ breadcrumbSchema: [],
+ },
+ argTypes: {
+ breadcrumbSchema: {
+ control: {
+ type: null,
+ },
+ description: 'The JSON schema for breadcrumb items.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ sections: {
+ description: 'The different sections.',
+ type: {
+ name: 'object',
+ required: true,
+ value: {},
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+ <LayoutBase {...LayoutBase.args}>
+ <Story />
+ </LayoutBase>
+ ),
+ ],
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof SectionedLayoutComponent>;
+
+const Template: ComponentStory<typeof SectionedLayoutComponent> = (args) => (
+ <SectionedLayoutComponent {...args} />
+);
+
+const sections = [
+ {
+ title: 'Section 1',
+ content:
+ 'Qui suscipit ea et aut dicta. Quia ut dignissimos. Sapiente beatae voluptatem quis et. Nemo vitae magni. Nihil iste officia est sed esse molestiae doloribus. Quia temporibus nobis ea fuga quis incidunt doloribus eaque.',
+ },
+ {
+ title: 'Section 2',
+ content:
+ 'Reprehenderit aut magnam ut quos. Voluptatibus beatae et. Earum non atque voluptatum illum rem distinctio repellat.',
+ },
+ {
+ title: 'Section 3',
+ content:
+ 'Placeat rem dolores dolore illum earum officia dolore. Ut est ducimus. Officia eveniet pariatur ut laboriosam voluptatibus aut doloremque natus quis.',
+ },
+ {
+ title: 'Section 4',
+ content:
+ 'Vitae facere ipsa eum sunt debitis veritatis dolorem labore qui. Dolores recusandae omnis aut. Repudiandae quia neque porro in blanditiis. A atque minima fugit. Totam quidem voluptas natus velit at.',
+ },
+];
+
+/**
+ * Sectioned Layout Stories - Default
+ */
+export const Sectioned = Template.bind({});
+Sectioned.args = {
+ sections,
+};
diff --git a/src/components/templates/sectioned/sectioned-layout.test.tsx b/src/components/templates/sectioned/sectioned-layout.test.tsx
new file mode 100644
index 0000000..9b8bab5
--- /dev/null
+++ b/src/components/templates/sectioned/sectioned-layout.test.tsx
@@ -0,0 +1,41 @@
+import { render, screen } from '@test-utils';
+import { BreadcrumbList } from 'schema-dts';
+import SectionedLayout from './sectioned-layout';
+
+const breadcrumbSchema: BreadcrumbList['itemListElement'][] = [];
+const sections = [
+ {
+ title: 'Section 1',
+ content:
+ 'Qui suscipit ea et aut dicta. Quia ut dignissimos. Sapiente beatae voluptatem quis et. Nemo vitae magni. Nihil iste officia est sed esse molestiae doloribus. Quia temporibus nobis ea fuga quis incidunt doloribus eaque.',
+ },
+ {
+ title: 'Section 2',
+ content:
+ 'Reprehenderit aut magnam ut quos. Voluptatibus beatae et. Earum non atque voluptatum illum rem distinctio repellat.',
+ },
+ {
+ title: 'Section 3',
+ content:
+ 'Placeat rem dolores dolore illum earum officia dolore. Ut est ducimus. Officia eveniet pariatur ut laboriosam voluptatibus aut doloremque natus quis.',
+ },
+ {
+ title: 'Section 4',
+ content:
+ 'Vitae facere ipsa eum sunt debitis veritatis dolorem labore qui. Dolores recusandae omnis aut. Repudiandae quia neque porro in blanditiis. A atque minima fugit. Totam quidem voluptas natus velit at.',
+ },
+];
+
+describe('SectionedLayout', () => {
+ it('renders the correct number of section', () => {
+ render(
+ <SectionedLayout
+ breadcrumbSchema={breadcrumbSchema}
+ sections={sections}
+ />
+ );
+ expect(screen.getAllByRole('heading', { name: /^Section/ })).toHaveLength(
+ sections.length
+ );
+ });
+});
diff --git a/src/components/templates/sectioned/sectioned-layout.tsx b/src/components/templates/sectioned/sectioned-layout.tsx
new file mode 100644
index 0000000..f91c354
--- /dev/null
+++ b/src/components/templates/sectioned/sectioned-layout.tsx
@@ -0,0 +1,60 @@
+import Section, {
+ type SectionProps,
+ type SectionVariant,
+} from '@components/atoms/layout/section';
+import Script from 'next/script';
+import { FC } from 'react';
+import { BreadcrumbList } from 'schema-dts';
+
+export type Section = Pick<SectionProps, 'content' | 'title'>;
+
+export type SectionedLayoutProps = {
+ /**
+ * The breadcrumb JSON schema.
+ */
+ breadcrumbSchema: BreadcrumbList['itemListElement'][];
+ /**
+ * An array of objects describing each section.
+ */
+ sections: Section[];
+};
+
+/**
+ * SectionedLayout component
+ *
+ * Render a sectioned layout.
+ */
+const SectionedLayout: FC<SectionedLayoutProps> = ({
+ breadcrumbSchema,
+ sections,
+}) => {
+ const getSections = (items: SectionProps[]) => {
+ return items.map((section, index) => {
+ const variant: SectionVariant = index % 2 ? 'light' : 'dark';
+ const isLastSection = index === items.length - 1;
+
+ return (
+ <Section
+ key={`section-${index}`}
+ title={section.title}
+ content={section.content}
+ variant={variant}
+ withBorder={!isLastSection}
+ />
+ );
+ });
+ };
+
+ return (
+ <>
+ <Script
+ id="schema-breadcrumb"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+ />
+ {getSections(sections)}
+ </>
+ );
+};
+
+export default SectionedLayout;
diff --git a/src/content b/src/content
-Subproject 52c97a48f39ef0de9a61d2cf120fae2c7079055
+Subproject 742cb70a219b324d8e4d9b7f0f48627984b5995
diff --git a/src/i18n/en.json b/src/i18n/en.json
index f6e48ae..417c19b 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1,8 +1,4 @@
{
- "+4tiVb": {
- "defaultMessage": "Others topics",
- "description": "TopicPage: topics list widget title"
- },
"+Dre5J": {
"defaultMessage": "Open-source projects",
"description": "CVPage: social media widget title"
@@ -11,29 +7,29 @@
"defaultMessage": "Blog: development, open source - {websiteName}",
"description": "BlogPage: SEO - Page title"
},
- "+aHn7j": {
- "defaultMessage": "Leave a comment",
- "description": "CommentForm: Form title"
+ "+dJU3e": {
+ "defaultMessage": "Comments",
+ "description": "PageLayout: comments title"
},
- "/IirIt": {
- "defaultMessage": "Legal notice",
- "description": "LegalNoticePage: page title"
+ "+viX9b": {
+ "defaultMessage": "Close settings",
+ "description": "Settings: Close label"
},
- "/ly3AC": {
- "defaultMessage": "Copy",
- "description": "Prism: copy button text (no clicked)"
+ "/42Z0z": {
+ "defaultMessage": "Related topics",
+ "description": "ThematicPage: related topics list widget title"
},
- "00Pf5p": {
- "defaultMessage": "Failed to load.",
- "description": "TopicsList: failed to load text"
+ "/q5csZ": {
+ "defaultMessage": "Animations:",
+ "description": "MotionToggle: reduce motion label"
},
- "0pp/IQ": {
- "defaultMessage": "{topicsCount, plural, =0 {Topics:} one {Topic:} other {Topics:}}",
- "description": "Topics: topics list meta label"
+ "/sRqPT": {
+ "defaultMessage": "Related thematics",
+ "description": "TopicPage: related thematics list widget title"
},
- "0zBQpa": {
- "defaultMessage": "Message",
- "description": "ContactForm: message field label"
+ "02rgLO": {
+ "defaultMessage": "{commentsCount, plural, =0 {No comments} one {# comment} other {# comments}}<a11y> about {title}</a11y>",
+ "description": "Meta: comments count"
},
"16zl9Z": {
"defaultMessage": "You are here:",
@@ -43,221 +39,217 @@
"defaultMessage": "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.",
"description": "BlogPage: SEO - Meta description"
},
- "1h+N2z": {
- "defaultMessage": "Published on:",
- "description": "RecentPosts: publication date label"
+ "1dCuCx": {
+ "defaultMessage": "Name:",
+ "description": "ContactForm: name label"
+ },
+ "28GZdv": {
+ "defaultMessage": "Projects",
+ "description": "Breadcrumb: projects label"
},
- "1r4ujR": {
- "defaultMessage": "{thematicsCount, plural, =0 {Thematics:} one {Thematic:} other {Thematics:}}",
- "description": "Thematics: thematics list meta label"
+ "28nnDY": {
+ "defaultMessage": "Breadcrumb",
+ "description": "Breadcrumb: an accessible name for the breadcrumb nav."
},
"2D9tB5": {
"defaultMessage": "Topics",
"description": "BlogPage: topics list widget title"
},
+ "2QwvtS": {
+ "defaultMessage": "Dark theme",
+ "description": "ThemeToggle: dark theme label"
+ },
+ "2fD5CI": {
+ "defaultMessage": "Leave a reply",
+ "description": "Comment: comment form title"
+ },
"2pmylc": {
"defaultMessage": "Tracking:",
"description": "AckeeSelect: select label"
},
- "2pykor": {
- "defaultMessage": "{title} picture",
- "description": "ProjectPreview: cover alt text"
- },
"310o3F": {
"defaultMessage": "Error 404: Page not found - {websiteName}",
"description": "404Page: SEO - Page title"
},
+ "3Pipok": {
+ "defaultMessage": "Thanks. Your message was successfully sent. I will answer it as soon as possible.",
+ "description": "Contact: success message"
+ },
+ "3f3PzH": {
+ "defaultMessage": "Github",
+ "description": "HomePage: Github link"
+ },
"48Ww//": {
"defaultMessage": "Page not found.",
"description": "404Page: SEO - Meta description"
},
- "4EMSLO": {
- "defaultMessage": "{total, plural, =0 {No articles} one {# article} other {# articles}}",
- "description": "PostCount: total found articles"
- },
- "4zAUSu": {
- "defaultMessage": "Legal notice - {websiteName}",
- "description": "LegalNoticePage: SEO - Page title"
+ "50xc4o": {
+ "defaultMessage": "Read more articles about:",
+ "description": "ArticlePage: footer topics list label"
},
- "52Fev1": {
- "defaultMessage": "Published on:",
- "description": "Dates: publication date meta label"
+ "5O2vpy": {
+ "defaultMessage": "No results found.",
+ "description": "NoResults: no results"
},
- "6BRtAu": {
- "defaultMessage": "Comments:",
- "description": "CommentsCount: comment count meta label"
+ "6GySNl": {
+ "defaultMessage": "Copy",
+ "description": "usePrism: copy button text (not clicked)"
},
- "6dXfvr": {
- "defaultMessage": "Table of Contents",
- "description": "ProjectPage: ToC sidebar aria-label"
+ "6a1Uo6": {
+ "defaultMessage": "This comment is awaiting moderation...",
+ "description": "Comment: awaiting moderation"
},
- "6ibqFS": {
- "defaultMessage": "Name",
- "description": "ContactForm: name field label"
+ "7AnwZ7": {
+ "defaultMessage": "Gitlab",
+ "description": "HomePage: Gitlab link"
},
"7TbbIk": {
"defaultMessage": "Blog",
"description": "BlogPage: page title"
},
- "7iiaRx": {
- "defaultMessage": "Facebook",
- "description": "Sharing: Facebook"
- },
- "8Ls2mD": {
- "defaultMessage": "Please fill the form to contact me.",
- "description": "ContactPage: page introduction"
+ "7jVUT6": {
+ "defaultMessage": "Warning: If you want to benefit from all features (search for example), please activate Javascript.",
+ "description": "Layout: noscript message"
},
- "8w+jnD": {
- "defaultMessage": "Blog - Page {number} - {websiteName}",
- "description": "BlogPage: SEO - Page title"
+ "92zgdp": {
+ "defaultMessage": "Total:",
+ "description": "Meta: total label"
},
- "9kx83j": {
- "defaultMessage": "Close help",
- "description": "Tooltip: button title"
+ "9MeLN3": {
+ "defaultMessage": "{articlesCount, plural, =0 {# loaded articles} one {# loaded article} other {# loaded articles}} out of a total of {total}",
+ "description": "PostsList: loaded articles progress"
},
- "9nhYRA": {
- "defaultMessage": "Table of Contents",
- "description": "ArticlePage: ToC sidebar aria-label"
+ "9sGNKq": {
+ "defaultMessage": "Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem.",
+ "description": "Error404Page: page body"
},
- "A4LTGq": {
- "defaultMessage": "Discover search results for {query}",
- "description": "SearchPage: meta description with query"
+ "A8hGaK": {
+ "defaultMessage": "Comment:",
+ "description": "CommentForm: comment label"
},
- "A5n+C9": {
- "defaultMessage": "Show help",
- "description": "Tooltip: button title"
+ "ADQmDF": {
+ "defaultMessage": "Technologies:",
+ "description": "Meta: technologies label"
},
- "AN9iy7": {
+ "AE4kCD": {
"defaultMessage": "Contact",
- "description": "ContactPage: page title"
+ "description": "Layout: main nav - contact link"
},
- "AVUUgG": {
- "defaultMessage": "Thanks for your comment!",
- "description": "CommentForm: success notice"
+ "AuGklx": {
+ "defaultMessage": "License:",
+ "description": "Meta: license label"
},
- "AnaPbu": {
- "defaultMessage": "Search",
- "description": "SearchForm: search button text"
+ "B290Ph": {
+ "defaultMessage": "Thanks, your comment was successfully sent.",
+ "description": "PageLayout: comment form success message"
},
"B9OCyV": {
"defaultMessage": "Others formats",
"description": "CVPage: cv preview widget title"
},
- "BAkq7J": {
- "defaultMessage": "Pagination",
- "description": "Pagination: pagination title"
- },
- "C+r/LF": {
- "defaultMessage": "Updated on:",
- "description": "Dates: update date meta label"
+ "Bh7z5v": {
+ "defaultMessage": "Email:",
+ "description": "CommentForm: email label"
},
"C/XGkH": {
"defaultMessage": "Failed to load.",
"description": "BlogPage: failed to load text"
},
- "CT3ydM": {
- "defaultMessage": "{date} at {time}",
- "description": "Comment: publication date"
- },
- "CWi0go": {
- "defaultMessage": "Created on:",
- "description": "ProjectSummary: creation date label"
- },
- "CzTbM4": {
- "defaultMessage": "Contact",
- "description": "ContactPage: breadcrumb item"
+ "D8vB38": {
+ "defaultMessage": "Blog",
+ "description": "Layout: main nav - blog link"
},
- "Dhow1m": {
- "defaultMessage": "Diaspora",
- "description": "Sharing: Diaspora"
+ "DVBwfu": {
+ "defaultMessage": "Would you like to try a new search?",
+ "description": "NoResults: try a new search message"
},
"Dq6+WH": {
"defaultMessage": "Thematics",
"description": "SearchPage: thematics list widget title"
},
- "Enij19": {
- "defaultMessage": "Home",
- "description": "Breadcrumb: Home item"
+ "DssFG1": {
+ "defaultMessage": "Repositories:",
+ "description": "Meta: repositories label"
},
- "EvODgw": {
- "defaultMessage": "Published on",
- "description": "PostsList: published on year label"
+ "EbFvsM": {
+ "defaultMessage": "Reading time:",
+ "description": "Meta: reading time label"
+ },
+ "Es52wh": {
+ "defaultMessage": "Blog",
+ "description": "Breadcrumb: blog label"
},
"F1EQX3": {
"defaultMessage": "Ackee tracking (analytics)",
"description": "AckeeSelect: tooltip title"
},
- "F7QxJH": {
- "defaultMessage": "Name",
- "description": "CommentForm: Name field label"
+ "G+Twgm": {
+ "defaultMessage": "Search",
+ "description": "SearchModal: modal title"
},
- "FLkF2R": {
- "defaultMessage": "All posts in {name}",
- "description": "TopicPage: posts list title"
+ "GRyyfy": {
+ "defaultMessage": "Official website:",
+ "description": "Meta: official website label"
},
- "GgIWnN": {
- "defaultMessage": "<a11y>Jump to </a11y>{title}",
- "description": "ToC: link"
+ "GTbGMy": {
+ "defaultMessage": "Open menu",
+ "description": "MainNav: Open label"
},
- "H7C5Bk": {
- "defaultMessage": "Primary",
- "description": "MainNav: aria-label"
+ "GVpTIl": {
+ "defaultMessage": "Topics",
+ "description": "Error404Page: topics list widget title"
},
- "HEJ3Gv": {
- "defaultMessage": "Submitting...",
- "description": "CommentForm: submitting message"
+ "Gnf1Si": {
+ "defaultMessage": "{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}",
+ "description": "Projets: Github stars count"
},
- "HTdaZj": {
- "defaultMessage": "Footer",
- "description": "FooterNav: aria-label"
+ "HFdzae": {
+ "defaultMessage": "Contact form",
+ "description": "ContactForm: form accessible name"
+ },
+ "HohQPh": {
+ "defaultMessage": "Thematics",
+ "description": "Error404Page: thematics list widget title"
},
"HriY57": {
"defaultMessage": "Thematics",
"description": "BlogPage: thematics list widget title"
},
- "ILRLTq": {
- "defaultMessage": "{brandingName} picture",
- "description": "Branding: branding name picture."
- },
- "IPs/Ck": {
- "defaultMessage": "Twitter",
- "description": "SocialMedia: Twitter"
- },
- "Igx3qp": {
- "defaultMessage": "Projects",
- "description": "Breadcrumb: Projects item"
+ "IY5ew6": {
+ "defaultMessage": "Submitting...",
+ "description": "CommentForm: spinner message on submit"
},
- "J4nhm4": {
- "defaultMessage": "Comment",
- "description": "CommentForm: Comment field label"
+ "JpC3JH": {
+ "defaultMessage": "Other topics",
+ "description": "TopicPage: other topics list widget title"
},
- "JeYOeA": {
- "defaultMessage": "Sidebar",
- "description": "ArticlePage: right sidebar aria-label"
+ "K4rYdT": {
+ "defaultMessage": "Skip to content",
+ "description": "Layout: Skip to content link"
},
- "JsOoAW": {
- "defaultMessage": "Website:",
- "description": "Website: website meta label"
+ "KUowUk": {
+ "defaultMessage": "{name} CV",
+ "description": "CVPage: CV image alternative text"
},
- "KERk7L": {
- "defaultMessage": "Filter by:",
- "description": "BlogPage: sidebar title"
+ "KVSWGP": {
+ "defaultMessage": "Other thematics",
+ "description": "ThematicPage: other thematics list widget title"
},
- "KeRtm/": {
- "defaultMessage": "Light theme",
- "description": "Icons: Sun icon (light theme)"
+ "KnWeKh": {
+ "defaultMessage": "Page not found",
+ "description": "Error404Page: page title"
},
- "Kqq2cm": {
- "defaultMessage": "Load more?",
- "description": "BlogPage: load more text"
+ "LCorTC": {
+ "defaultMessage": "Cancel reply",
+ "description": "Comment: cancel reply button"
},
- "LR70nt": {
- "defaultMessage": "Without Javascript, some features may not work like loading more posts or use search. If you want to benefit from these features, please activate Javascript.",
- "description": "Layout: noscript banner"
+ "LDDUNO": {
+ "defaultMessage": "Close search",
+ "description": "Search: Close label"
},
- "Mj2BQf": {
- "defaultMessage": "{name}'s CV",
- "description": "CVPage: page title"
+ "LszkU6": {
+ "defaultMessage": "All posts in {thematicName}",
+ "description": "ThematicPage: posts list heading"
},
"N44SOc": {
"defaultMessage": "Projects",
@@ -267,97 +259,61 @@
"defaultMessage": "Topics",
"description": "SearchPage: topics list widget title"
},
- "Ns8CFb": {
- "defaultMessage": "Comments",
- "description": "CommentsList: Comments section title"
- },
- "O9XLDc": {
- "defaultMessage": "Theme:",
- "description": "ThemeToggle: toggle label"
- },
- "OIffB4": {
- "defaultMessage": "Contact {websiteName} through its website. All you need to do it's to fill the contact form.",
- "description": "ContactPage: SEO - Meta description"
+ "OF5cPz": {
+ "defaultMessage": "{postsCount, plural, =0 {No articles} one {# article} other {# articles}}",
+ "description": "BlogPage: posts count meta"
},
- "OTTv+m": {
- "defaultMessage": "{count, plural, =0 {Repositories:} one {Repository:} other {Repositories:}}",
- "description": "ProjectSummary: repositories list label"
+ "OI0N37": {
+ "defaultMessage": "Written by:",
+ "description": "Meta: author label"
},
- "OV9r1K": {
- "defaultMessage": "Copied!",
- "description": "Prism: copy button text (clicked)"
+ "OL0Yzx": {
+ "defaultMessage": "Publish",
+ "description": "CommentForm: submit button"
},
- "OccTWi": {
- "defaultMessage": "Page not found",
- "description": "404Page: page title"
+ "OevMeU": {
+ "defaultMessage": "{minutesCount} minutes {secondsCount} seconds",
+ "description": "useReadingTime: minutes + seconds count"
},
"Ogccx6": {
"defaultMessage": "Full includes all information from partial as well as information about referrer, operating system, device, browser, screen size and language.",
"description": "AckeeSelect: tooltip message"
},
- "Oim3rQ": {
- "defaultMessage": "Email",
- "description": "CommentForm: Email field label"
- },
- "P0I+Xm": {
- "defaultMessage": "Journal du hacker",
- "description": "Sharing: Journal du hacker"
- },
- "P7fxX2": {
- "defaultMessage": "All posts in {name}",
- "description": "ThematicPage: posts list title"
- },
"PXp2hv": {
"defaultMessage": "{websiteName} | Front-end developer: WordPress/React",
"description": "HomePage: SEO - Page title"
},
- "PrIz5o": {
- "defaultMessage": "Search for a post on {websiteName}",
- "description": "SearchPage: meta description without query"
- },
- "PxMDzL": {
- "defaultMessage": "Failed to load.",
- "description": "ThematicsList: failed to load text"
- },
"PzRpPw": {
"defaultMessage": "Full",
"description": "AckeeSelect: full option name"
},
- "QHOm5t": {
- "defaultMessage": "Sidebar",
- "description": "CVPage: right sidebar aria-label"
+ "Q+1GbT": {
+ "defaultMessage": "Table of contents sidebar",
+ "description": "PageLayout: accessible name for ToC sidebar"
+ },
+ "QCW3cy": {
+ "defaultMessage": "Open settings",
+ "description": "Settings: Open label"
+ },
+ "QGi5uD": {
+ "defaultMessage": "Published on:",
+ "description": "Meta: publication date label"
+ },
+ "QLisK6": {
+ "defaultMessage": "Dark Theme 🌙",
+ "description": "usePrism: toggle dark theme button text"
},
"Qh2CwH": {
"defaultMessage": "Find me elsewhere",
"description": "ContactPage: social media widget title"
},
- "R0eDmw": {
- "defaultMessage": "Blog",
- "description": "BlogPage: breadcrumb item"
- },
"R4yaW6": {
"defaultMessage": "Next page {icon}",
"description": "Pagination: Next page link"
},
- "RZzx/4": {
- "defaultMessage": "Javascript is required to use the table of contents.",
- "description": "ToC: noscript tag"
- },
- "Rle+UK": {
- "defaultMessage": "Some required fields are empty. Comment cannot be submitted.",
- "description": "CommentForm: missing required fields"
- },
- "SWjj4l": {
- "defaultMessage": "Github",
- "description": "SocialMedia: Github"
- },
- "SWq8a4": {
- "defaultMessage": "Close {type}",
- "description": "ButtonToolbar: Close button"
- },
- "SX1z3t": {
- "defaultMessage": "Projects: open-source makings - {websiteName}",
- "description": "ProjectsPage: SEO - Page title"
+ "R895yC": {
+ "defaultMessage": "CV",
+ "description": "Layout: main nav - cv link"
},
"T4YA64": {
"defaultMessage": "Subscribe",
@@ -367,305 +323,225 @@
"defaultMessage": "<a11y>Page </a11y>{number}",
"description": "Pagination: page number"
},
- "TfU6Qm": {
- "defaultMessage": "Search",
- "description": "SearchPage: breadcrumb item"
- },
- "U+35YD": {
- "defaultMessage": "Search",
- "description": "SearchPage: page title"
- },
- "Ua2g2p": {
- "defaultMessage": "Light Theme 🌞",
- "description": "Prism: toggle light theme button text"
+ "TpyFZ6": {
+ "defaultMessage": "An error occurred:",
+ "description": "Contact: error message"
},
- "Ul2NIl": {
- "defaultMessage": "Thanks for your comment! It is now awaiting moderation.",
- "description": "CommentForm: success notice but awaiting moderation"
+ "UX9Bu8": {
+ "defaultMessage": "Collapse",
+ "description": "HeadingButton: title prefix (expanded state)"
},
"UsQske": {
"defaultMessage": "Read more here:",
"description": "Sharing: content link prefix"
},
- "VSGuGE": {
- "defaultMessage": "Search results for {query}",
- "description": "SearchPage: search results text"
- },
- "VbcHZ4": {
- "defaultMessage": "LinkedIn",
- "description": "SocialMedia: LinkedIn"
- },
- "Vuryko": {
- "defaultMessage": "Email",
- "description": "ContactForm: email field label"
- },
- "WGFOmA": {
+ "VkAnvv": {
"defaultMessage": "Send",
- "description": "CommentForm: Send button"
- },
- "WRkY1/": {
- "defaultMessage": "Collapse",
- "description": "ExpandableWidget: collapse text"
+ "description": "ContactForm: send button"
},
- "WjVBnY": {
- "defaultMessage": "Twitter",
- "description": "Sharing: Twitter"
- },
- "WpycgB": {
- "defaultMessage": "Warning: mail not sent. Some required fields are empty.",
- "description": "ContactForm: missing fields message."
- },
- "X3PDXO": {
- "defaultMessage": "Animations:",
- "description": "ReduceMotion: toggle label"
+ "Vmj5cw": {
+ "defaultMessage": "It is now awaiting moderation.",
+ "description": "PageLayout: comment awaiting moderation"
},
- "X7n7N2": {
- "defaultMessage": "Send",
- "description": "ContactForm: send button text"
- },
- "Y1ZdJ6": {
- "defaultMessage": "CV Front-end developer - {websiteName}",
- "description": "CVPage: SEO - Page title"
+ "WDwNDl": {
+ "defaultMessage": "Search",
+ "description": "SearchPage: SEO - Page title"
},
- "Y3qRib": {
- "defaultMessage": "Contact form - {websiteName}",
- "description": "ContactPage: SEO - Page title"
+ "WKG9wj": {
+ "defaultMessage": "Table of Contents",
+ "description": "TableOfContents: the widget title"
},
- "YEudoh": {
- "defaultMessage": "Read more articles about:",
- "description": "PostFooter: read more posts about given subjects"
+ "WMqQrv": {
+ "defaultMessage": "Search",
+ "description": "SearchForm: button accessible name"
},
- "YvMPuD": {
- "defaultMessage": "Keywords:",
- "description": "SearchForm: search field label"
+ "X8oujO": {
+ "defaultMessage": "Search for:",
+ "description": "SearchForm: field accessible label"
},
- "YwvYfw": {
- "defaultMessage": "Table of Contents",
- "description": "ThematicPage: ToC sidebar aria-label"
+ "XKy7rx": {
+ "defaultMessage": "You can also try a search:",
+ "description": "Error404Page: try a search message"
},
- "Z1eSIz": {
- "defaultMessage": "Open {type}",
- "description": "ButtonToolbar: Open button"
+ "Xj+WXB": {
+ "defaultMessage": "Open search",
+ "description": "Search: Open label"
},
- "ZJMNRW": {
- "defaultMessage": "Home",
- "description": "MainNav: home link"
+ "Ygea7s": {
+ "defaultMessage": "Light theme",
+ "description": "ThemeToggle: light theme label"
},
- "ZWh78Y": {
- "defaultMessage": "Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem.",
- "description": "404Page: page body"
+ "ZIrTee": {
+ "defaultMessage": "Name:",
+ "description": "CommentForm: name label"
},
- "Zg4L7U": {
- "defaultMessage": "Table of contents",
- "description": "ToC: widget title"
+ "ZNBhDP": {
+ "defaultMessage": "Search results for {query}",
+ "description": "SearchPage: SEO - Page title"
},
- "Zlkww3": {
- "defaultMessage": "Failed to load.",
- "description": "CommentsList: failed to load"
+ "Zpgv+f": {
+ "defaultMessage": "Read more<a11y> about {title}</a11y>",
+ "description": "Summary: read more link"
},
- "aA3hOT": {
- "defaultMessage": "{starsCount, plural, =0 {0 stars on Github} one {# star on Github} other {# stars on Github}}",
- "description": "ProjectSummary: technologies list label"
+ "aJC7D2": {
+ "defaultMessage": "Close menu",
+ "description": "MainNav: Close label"
},
"aMFqPH": {
"defaultMessage": "{icon} Previous page",
"description": "Pagination: previous page link"
},
- "akSutM": {
- "defaultMessage": "Projects",
- "description": "MainNav: projects link"
- },
- "azc1GT": {
- "defaultMessage": "Open menu",
- "description": "MainNav: open button"
+ "azgQuH": {
+ "defaultMessage": "You should read {title}",
+ "description": "Sharing: subject text"
},
- "bBdMGm": {
- "defaultMessage": "Discover the curriculum of {websiteName}, front-end developer located in France: skills, experiences and training.",
- "description": "CVPage: SEO - Meta description"
+ "b4fdYE": {
+ "defaultMessage": "Created on:",
+ "description": "Meta: creation date label"
},
- "bHEmkY": {
- "defaultMessage": "Settings",
- "description": "Settings: modal title"
+ "bcyOgC": {
+ "defaultMessage": "Expand",
+ "description": "HeadingButton: title prefix (collapsed state)"
},
- "bkbrN7": {
- "defaultMessage": "Read more<a11y> about {title}</a11y>",
- "description": "PostPreview: read more link"
+ "bojYF5": {
+ "defaultMessage": "Home",
+ "description": "Layout: main nav - home link"
},
- "c2NtPj": {
- "defaultMessage": "Contact",
- "description": "MainNav: contact link"
+ "bz53Us": {
+ "defaultMessage": "Thematics:",
+ "description": "Meta: thematics label"
},
- "cjK9Ad": {
- "defaultMessage": "An unexpected error happened. Comment cannot be submitted.",
- "description": "CommentForm: error notice"
+ "c556Qo": {
+ "defaultMessage": "Sidebar",
+ "description": "PageLayout: accessible name for the sidebar"
},
- "csCQQk": {
- "defaultMessage": "LinkedIn",
- "description": "Sharing: LinkedIn"
+ "cl7YNU": {
+ "defaultMessage": "CC BY SA",
+ "description": "CCBySA: icon title"
},
- "dE8xxV": {
- "defaultMessage": "Close menu",
- "description": "MainNav: close button"
+ "d4N8nD": {
+ "defaultMessage": "Footer",
+ "description": "Footer: an accessible name for footer nav"
},
- "dqrd6I": {
- "defaultMessage": "Back to top",
- "description": "Footer: Back to top button"
+ "dDK5oc": {
+ "defaultMessage": "{website} picture",
+ "description": "Branding: photo alternative text"
},
- "du4MLN": {
- "defaultMessage": "{articlesCount, plural, =0 {# loaded articles} one {# loaded article} other {# loaded articles}} out of a total of {total}",
- "description": "PaginationCursor: loaded articles count message"
+ "dz2kDV": {
+ "defaultMessage": "Comment form",
+ "description": "CommentForm: aria label"
},
"e/8Kyj": {
"defaultMessage": "Partial",
"description": "AckeeSelect: partial option name"
},
- "e1Forh": {
- "defaultMessage": "Cancel reply",
- "description": "Comment: reply button"
- },
- "e9L59q": {
- "defaultMessage": "No comments yet.",
- "description": "CommentsList: No comment message"
- },
- "eFMu2E": {
- "defaultMessage": "Search",
- "description": "SearchForm : form title"
- },
- "eUXMG4": {
- "defaultMessage": "Seen on {domainName}:",
- "description": "Sharing: seen on text"
- },
- "enwhNm": {
- "defaultMessage": "{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}",
- "description": "ProjectSummary: technologies list label"
- },
- "eu3beS": {
- "defaultMessage": "Sidebar",
- "description": "TopicPage: right sidebar aria-label"
+ "fN04AJ": {
+ "defaultMessage": "<link>Download the CV in PDF</link>",
+ "description": "CVPage: download CV in PDF text"
},
"fOe8rH": {
"defaultMessage": "Failed to load.",
"description": "SearchPage: failed to load text"
},
- "g1cFCa": {
- "defaultMessage": "Javascript is required to post a comment.",
- "description": "CommentForm: noscript tag"
+ "fcHeyC": {
+ "defaultMessage": "{date} at {time}",
+ "description": "Meta: publication date and time"
},
- "g4DckL": {
- "defaultMessage": "Table of Contents",
- "description": "CVPage: ToC sidebar aria-label"
+ "fkcTGp": {
+ "defaultMessage": "An error occurred:",
+ "description": "PageLayout: comment form error message"
},
- "gQKeF+": {
- "defaultMessage": "Thanks. Your message was successfully sent. I will answer it as soon as possible.",
- "description": "ContactForm: success message"
+ "ftXN+0": {
+ "defaultMessage": "Code blocks:",
+ "description": "PrismThemeToggle: theme label"
},
- "hHrNd0": {
- "defaultMessage": "Sidebar",
- "description": "ProjectPage: right sidebar aria-label"
+ "g3+Ahv": {
+ "defaultMessage": "It has been approved.",
+ "description": "PageLayout: comment approved."
},
- "hKagVG": {
- "defaultMessage": "License:",
- "description": "ProjectSummary: license label"
+ "gJNaBD": {
+ "defaultMessage": "Topics:",
+ "description": "Meta: topics label"
},
- "hV0qHp": {
- "defaultMessage": "Expand",
- "description": "ExpandableWidget: expand text"
+ "gPfT/K": {
+ "defaultMessage": "Settings",
+ "description": "SettingsModal: title"
+ },
+ "gX+YVy": {
+ "defaultMessage": "Settings form",
+ "description": "SettingsForm: an accessible form name"
+ },
+ "hHVgW3": {
+ "defaultMessage": "Light Theme 🌞",
+ "description": "usePrism: toggle light theme button text"
},
"hzHuCc": {
"defaultMessage": "Reply",
"description": "Comment: reply button"
},
+ "i+/ckF": {
+ "defaultMessage": "Help",
+ "description": "HelpButton: screen reader text"
+ },
"i5L19t": {
"defaultMessage": "Shaarli",
"description": "HomePage: link to Shaarli"
},
- "iqAbyn": {
- "defaultMessage": "Skip to content",
- "description": "Layout: Skip to content button"
+ "i7Wq3G": {
+ "defaultMessage": "{count} seconds",
+ "description": "useReadingTime: seconds count"
},
- "iyEh0R": {
- "defaultMessage": "Failed to load.",
- "description": "RecentPosts: failed to load text"
+ "j5k9Fe": {
+ "defaultMessage": "Home",
+ "description": "Breadcrumb: home label"
},
"jASD7k": {
"defaultMessage": "Linux",
"description": "HomePage: link to Linux thematic"
},
- "jCyqZS": {
- "defaultMessage": "Written by:",
- "description": "Author: article author meta label"
- },
- "jN+dY5": {
- "defaultMessage": "Website",
- "description": "CommentForm: Website field label"
- },
- "jpv+Nz": {
- "defaultMessage": "Resume",
- "description": "MainNav: resume link"
- },
- "k7/SkN": {
- "defaultMessage": "Approximately {number} words",
- "description": "ReadingTime: number of words"
- },
- "lKGNKx": {
- "defaultMessage": "{total, plural, =0 {No comments} one {# comment} other {# comments}}",
- "description": "CommentsCount: comment count value"
- },
- "lKZm9t": {
- "defaultMessage": "Email",
- "description": "Sharing: Email"
- },
- "lsDB5G": {
- "defaultMessage": "Table of Contents",
- "description": "TopicPage: ToC sidebar aria-label"
- },
- "mC21ht": {
- "defaultMessage": "Number of articles loaded out of the total available.",
- "description": "PaginationCursor: loaded articles count aria-label"
+ "jTVIh8": {
+ "defaultMessage": "Comments:",
+ "description": "Meta: comments label"
},
- "mh7tGg": {
- "defaultMessage": "{title} preview",
- "description": "ProjectSummary: cover alt text"
+ "kzIYoQ": {
+ "defaultMessage": "Leave a comment",
+ "description": "PageLayout: comment form title"
},
- "n0Gbod": {
- "defaultMessage": "Reading time:",
- "description": "ReadingTime: reading time meta label"
+ "lKhTGM": {
+ "defaultMessage": "Use Ctrl+c to copy",
+ "description": "usePrism: copy button error text"
},
- "nFMdWI": {
- "defaultMessage": "Dark Theme 🌙",
- "description": "Prism: toggle dark theme button text"
+ "m+SUSR": {
+ "defaultMessage": "Back to top",
+ "description": "BackToTop: link text"
},
- "norrGp": {
- "defaultMessage": "Others thematics",
- "description": "ThematicPage: thematics list widget title"
+ "npisb3": {
+ "defaultMessage": "Search for a post on {websiteName}.",
+ "description": "SearchPage: SEO - Meta description"
},
- "oPf+XA": {
- "defaultMessage": "Help",
- "description": "ButtonHelp: screen reader text"
+ "nsw6Th": {
+ "defaultMessage": "Copied!",
+ "description": "usePrism: copy button text (clicked)"
},
- "obmlFh": {
- "defaultMessage": "Gitlab",
- "description": "SocialMedia: Gitlab"
+ "nwbzKm": {
+ "defaultMessage": "Legal notice",
+ "description": "Layout: Legal notice label"
},
- "ode0YK": {
+ "og/zWL": {
"defaultMessage": "Dark theme",
- "description": "Icons: Moon icon (dark theme)"
- },
- "okFrAO": {
- "defaultMessage": "{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}",
- "description": "ProjectPreview: technologies list label"
+ "description": "PrismThemeToggle: dark theme label"
},
- "p1zZ/Z": {
- "defaultMessage": "Total:",
- "description": "PostCount: total found articles meta label"
+ "pWKyyR": {
+ "defaultMessage": "Off",
+ "description": "MotionToggle: deactivate reduce motion label"
},
- "pEtJik": {
- "defaultMessage": "Load more?",
- "description": "SearchPage: load more text"
+ "pWTj2W": {
+ "defaultMessage": "Popularity:",
+ "description": "Meta: popularity label"
},
- "pTxT7N": {
- "defaultMessage": "An error occurred:",
- "description": "ContactForm: error message"
+ "pg26sn": {
+ "defaultMessage": "Discover search results for {query} on {websiteName}.",
+ "description": "SearchPage: SEO - Meta description"
},
"q3U6uI": {
"defaultMessage": "Share",
@@ -675,21 +551,21 @@
"defaultMessage": "Loading...",
"description": "Spinner: loading text"
},
- "qPU/Qn": {
- "defaultMessage": "On",
- "description": "ReduceMotion: toggle on label"
+ "qnwsWV": {
+ "defaultMessage": "Projects",
+ "description": "Layout: main nav - projects link"
},
- "qXQETZ": {
- "defaultMessage": "{thematicsCount, plural, =0 {Related thematics} one {Related thematic} other {Related thematics}}",
- "description": "RelatedThematics: widget title"
+ "s1i43J": {
+ "defaultMessage": "{minutesCount} minutes",
+ "description": "useReadingTime: rounded minutes count"
},
- "rXeTkM": {
- "defaultMessage": "This comment is awaiting moderation.",
- "description": "Comment: awaiting moderation message"
+ "s8/tyz": {
+ "defaultMessage": "Object:",
+ "description": "ContactForm: object label"
},
- "s6U1Xt": {
- "defaultMessage": "Discover {websiteName} projects. Mostly related to web development and open source.",
- "description": "ProjectsPage: SEO - Meta description"
+ "sBwfCy": {
+ "defaultMessage": "No comments.",
+ "description": "PageLayout: no comments text"
},
"sO/Iwj": {
"defaultMessage": "Contact me",
@@ -699,100 +575,76 @@
"defaultMessage": "Partial includes only page url, views and duration.",
"description": "AckeeSelect: tooltip message"
},
- "soj7do": {
- "defaultMessage": "Published on:",
- "description": "Comment: publication date label"
+ "suXOBu": {
+ "defaultMessage": "Theme:",
+ "description": "ThemeToggle: theme label"
},
- "syLgY9": {
- "defaultMessage": "Sidebar",
- "description": "ThematicPage: right sidebar aria-label"
+ "tLC7bh": {
+ "defaultMessage": "Updated on:",
+ "description": "Meta: update date label"
},
"tMuNTy": {
"defaultMessage": "{websiteName} is a front-end developer located in France. He codes and he writes mostly about web development and open-source.",
"description": "HomePage: SEO - Meta description"
},
- "txusHd": {
- "defaultMessage": "All fields marked with * are required.",
- "description": "ContactPage: required fields text"
+ "tsWh8x": {
+ "defaultMessage": "Light theme",
+ "description": "PrismThemeToggle: light theme label"
},
- "uMURuJ": {
- "defaultMessage": "Subject",
- "description": "ContactForm: subject field label"
+ "u41qSk": {
+ "defaultMessage": "Website:",
+ "description": "CommentForm: website label"
+ },
+ "uaqd5F": {
+ "defaultMessage": "Load more articles?",
+ "description": "PostsList: load more button"
},
"ureXFw": {
"defaultMessage": "Share on {name}",
"description": "Sharing: share on social network text"
},
- "uvB+32": {
- "defaultMessage": "Discover the legal notice of {websiteName}'s website.",
- "description": "LegalNoticePage: SEO - Meta description"
- },
- "vJ+QDV": {
- "defaultMessage": "Last updated on:",
- "description": "ProjectSummary: update date label"
- },
- "vK7Sxv": {
- "defaultMessage": "No results found.",
- "description": "PostsList: no results"
- },
- "vgMk0q": {
- "defaultMessage": "Popularity:",
- "description": "ProjectSummary: popularity label"
+ "va65iw": {
+ "defaultMessage": "On",
+ "description": "MotionToggle: activate reduce motion label"
},
"vkF/RP": {
"defaultMessage": "Web development",
"description": "HomePage: link to web development thematic"
},
- "w/lPUh": {
- "defaultMessage": "{topicsCount, plural, =0 {Related topics} one {Related topic} other {Related topics}}",
- "description": "RelatedTopics: widget title"
- },
- "w0UfY0": {
- "defaultMessage": "Code blocks:",
- "description": "PrismThemeToggle: toggle label"
- },
- "w1nIrj": {
- "defaultMessage": "Off",
- "description": "ReduceMotion: toggle off label"
+ "w4B5PA": {
+ "defaultMessage": "Email:",
+ "description": "ContactForm: email label"
},
"w8GrOf": {
"defaultMessage": "Free",
"description": "HomePage: link to free thematic"
},
- "wdqOpf": {
- "defaultMessage": "{time, plural, =0 {# minutes} one {# minute} other {# minutes}}",
- "description": "ReadingTime: reading time value"
+ "x55qsD": {
+ "defaultMessage": "{website} logo",
+ "description": "Branding: logo title"
},
- "xC3Khf": {
- "defaultMessage": "Download <link>CV in PDF</link>",
- "description": "CVPreview: download as PDF link"
+ "xaqaYQ": {
+ "defaultMessage": "Sending mail...",
+ "description": "ContactForm: spinner message on submit"
},
- "ySsWZl": {
- "defaultMessage": "less than 1 minute",
- "description": "ReadingTime: Reading time value"
+ "yE/Jdz": {
+ "defaultMessage": "You are here:",
+ "description": "Pagination: current page indication"
},
- "yWjXRx": {
- "defaultMessage": "Legal notice",
- "description": "FooterNav: legal notice link"
+ "yN5P+m": {
+ "defaultMessage": "Message:",
+ "description": "ContactForm: message label"
},
"yfgMcl": {
"defaultMessage": "Introduction:",
"description": "Sharing: email content prefix"
},
- "ywkCsK": {
- "defaultMessage": "Error 404",
- "description": "404Page: breadcrumb item"
- },
- "z0ic9c": {
- "defaultMessage": "Blog",
- "description": "Breadcrumb: Blog item"
- },
- "z9qkcQ": {
- "defaultMessage": "Use Ctrl+c to copy",
- "description": "Prism: error text"
+ "zEN3fd": {
+ "defaultMessage": "All posts in {topicName}",
+ "description": "TopicPage: posts list heading"
},
- "zPJifH": {
- "defaultMessage": "Blog",
- "description": "MainNav: blog link"
+ "zbzlb1": {
+ "defaultMessage": "Page {number}",
+ "description": "BlogPage: page number"
}
}
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 6f8ce41..539afaa 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -1,8 +1,4 @@
{
- "+4tiVb": {
- "defaultMessage": "Autres sujets",
- "description": "TopicPage: topics list widget title"
- },
"+Dre5J": {
"defaultMessage": "Projets open-source",
"description": "CVPage: social media widget title"
@@ -11,29 +7,29 @@
"defaultMessage": "Blog : développement, libre et open-source - {websiteName}",
"description": "BlogPage: SEO - Page title"
},
- "+aHn7j": {
- "defaultMessage": "Laisser un commentaire",
- "description": "CommentForm: Form title"
+ "+dJU3e": {
+ "defaultMessage": "Commentaires",
+ "description": "PageLayout: comments title"
},
- "/IirIt": {
- "defaultMessage": "Mentions légales",
- "description": "LegalNoticePage: page title"
+ "+viX9b": {
+ "defaultMessage": "Fermer les réglages",
+ "description": "Settings: Close label"
},
- "/ly3AC": {
- "defaultMessage": "Copier",
- "description": "Prism: copy button text (no clicked)"
+ "/42Z0z": {
+ "defaultMessage": "Sujets liés",
+ "description": "ThematicPage: related topics list widget title"
},
- "00Pf5p": {
- "defaultMessage": "Échec du chargement.",
- "description": "TopicsList: failed to load text"
+ "/q5csZ": {
+ "defaultMessage": "Animations :",
+ "description": "MotionToggle: reduce motion label"
},
- "0pp/IQ": {
- "defaultMessage": "{topicsCount, plural, =0 {Sujet :} one {Sujet :} other {Sujets :}}",
- "description": "Topics: topics list meta label"
+ "/sRqPT": {
+ "defaultMessage": "Thématiques liées",
+ "description": "TopicPage: related thematics list widget title"
},
- "0zBQpa": {
- "defaultMessage": "Message",
- "description": "ContactForm: message field label"
+ "02rgLO": {
+ "defaultMessage": "{commentsCount, plural, =0 {0 commentaire} one {# commentaire} other {# commentaires}}<a11y> à propos de {title}</a11y>",
+ "description": "Meta: comments count"
},
"16zl9Z": {
"defaultMessage": "Vous êtes ici :",
@@ -43,221 +39,217 @@
"defaultMessage": "Découvrez les articles d'{websiteName}. Il écrit à propos de développement web, de Linux et du libre essentiellement.",
"description": "BlogPage: SEO - Meta description"
},
- "1h+N2z": {
- "defaultMessage": "Publié le :",
- "description": "RecentPosts: publication date label"
+ "1dCuCx": {
+ "defaultMessage": "Nom :",
+ "description": "ContactForm: name label"
+ },
+ "28GZdv": {
+ "defaultMessage": "Projets",
+ "description": "Breadcrumb: projects label"
},
- "1r4ujR": {
- "defaultMessage": "{thematicsCount, plural, =0 {Thématique :} one {Thématique :} other {Thématiques :}}",
- "description": "Thematics: thematics list meta label"
+ "28nnDY": {
+ "defaultMessage": "Fil d'Ariane",
+ "description": "Breadcrumb: an accessible name for the breadcrumb nav."
},
"2D9tB5": {
"defaultMessage": "Sujets",
"description": "BlogPage: topics list widget title"
},
+ "2QwvtS": {
+ "defaultMessage": "Thème sombre",
+ "description": "ThemeToggle: dark theme label"
+ },
+ "2fD5CI": {
+ "defaultMessage": "Laisser une réponse",
+ "description": "Comment: comment form title"
+ },
"2pmylc": {
"defaultMessage": "Suivi :",
"description": "AckeeSelect: select label"
},
- "2pykor": {
- "defaultMessage": "Image de {title}",
- "description": "ProjectPreview: cover alt text"
- },
"310o3F": {
"defaultMessage": "Erreur 404 : Page non trouvée - {websiteName}",
"description": "404Page: SEO - Page title"
},
+ "3Pipok": {
+ "defaultMessage": "Merici. Votre message a été envoyé avec succès. J'y répondrais aussi rapidement que possible..",
+ "description": "Contact: success message"
+ },
+ "3f3PzH": {
+ "defaultMessage": "Github",
+ "description": "HomePage: Github link"
+ },
"48Ww//": {
"defaultMessage": "Page non trouvée.",
"description": "404Page: SEO - Meta description"
},
- "4EMSLO": {
- "defaultMessage": "{total, plural, =0 {Aucun article} one {# article} other {# articles}}",
- "description": "PostCount: total found articles"
- },
- "4zAUSu": {
- "defaultMessage": "Mentions légales - {websiteName}",
- "description": "LegalNoticePage: SEO - Page title"
+ "50xc4o": {
+ "defaultMessage": "Lire plus d'articles à propos de :",
+ "description": "ArticlePage: footer topics list label"
},
- "52Fev1": {
- "defaultMessage": "Publié le :",
- "description": "Dates: publication date meta label"
+ "5O2vpy": {
+ "defaultMessage": "Aucun résultat.",
+ "description": "NoResults: no results"
},
- "6BRtAu": {
- "defaultMessage": "Commentaires :",
- "description": "CommentsCount: comment count meta label"
+ "6GySNl": {
+ "defaultMessage": "Copier",
+ "description": "usePrism: copy button text (not clicked)"
},
- "6dXfvr": {
- "defaultMessage": "Table des matières",
- "description": "ProjectPage: ToC sidebar aria-label"
+ "6a1Uo6": {
+ "defaultMessage": "Ce commentaire est en attente de modération...",
+ "description": "Comment: awaiting moderation"
},
- "6ibqFS": {
- "defaultMessage": "Nom",
- "description": "ContactForm: name field label"
+ "7AnwZ7": {
+ "defaultMessage": "Gitlab",
+ "description": "HomePage: Gitlab link"
},
"7TbbIk": {
"defaultMessage": "Blog",
"description": "BlogPage: page title"
},
- "7iiaRx": {
- "defaultMessage": "Facebook",
- "description": "Sharing: Facebook"
- },
- "8Ls2mD": {
- "defaultMessage": "Veuillez remplir le formulaire pour me contacter.",
- "description": "ContactPage: page introduction"
+ "7jVUT6": {
+ "defaultMessage": "Attention: Si vous souhaitez bénéficier de toutes les fonctionnalités (la recherche par exemple), veuillez activer Javascript.",
+ "description": "Layout: noscript message"
},
- "8w+jnD": {
- "defaultMessage": "Blog - Page {number} - {websiteName}",
- "description": "BlogPage: SEO - Page title"
+ "92zgdp": {
+ "defaultMessage": "Total :",
+ "description": "Meta: total label"
},
- "9kx83j": {
- "defaultMessage": "Fermer l'aide",
- "description": "Tooltip: button title"
+ "9MeLN3": {
+ "defaultMessage": "{articlesCount, plural, =0 {# article chargé} one {# article chargé} other {# articles chargés}} sur un total de {total}",
+ "description": "PostsList: loaded articles progress"
},
- "9nhYRA": {
- "defaultMessage": "Table des matières",
- "description": "ArticlePage: ToC sidebar aria-label"
+ "9sGNKq": {
+ "defaultMessage": "Désolé, il semble que la page demandée n'existe pas. Si vous pensez que le chemin devrait exister, n'hésitez pas à <link>me contacter</link> avec les informations nécessaires pour que je puisse corriger le problème.",
+ "description": "Error404Page: page body"
},
- "A4LTGq": {
- "defaultMessage": "Découvrez les résultats de recherche pour {query}",
- "description": "SearchPage: meta description with query"
+ "A8hGaK": {
+ "defaultMessage": "Commentaire :",
+ "description": "CommentForm: comment label"
},
- "A5n+C9": {
- "defaultMessage": "Afficher l'aide",
- "description": "Tooltip: button title"
+ "ADQmDF": {
+ "defaultMessage": "Technologies :",
+ "description": "Meta: technologies label"
},
- "AN9iy7": {
+ "AE4kCD": {
"defaultMessage": "Contact",
- "description": "ContactPage: page title"
+ "description": "Layout: main nav - contact link"
},
- "AVUUgG": {
- "defaultMessage": "Merci pour votre commentaire !",
- "description": "CommentForm: success notice"
+ "AuGklx": {
+ "defaultMessage": "Licence :",
+ "description": "Meta: license label"
},
- "AnaPbu": {
- "defaultMessage": "Rechercher",
- "description": "SearchForm: search button text"
+ "B290Ph": {
+ "defaultMessage": "Merci, votre commentaire a été envoyé avec succès.",
+ "description": "PageLayout: comment form success message"
},
"B9OCyV": {
"defaultMessage": "Autres formats",
"description": "CVPage: cv preview widget title"
},
- "BAkq7J": {
- "defaultMessage": "Pagination",
- "description": "Pagination: pagination title"
- },
- "C+r/LF": {
- "defaultMessage": "Mis à jour le :",
- "description": "Dates: update date meta label"
+ "Bh7z5v": {
+ "defaultMessage": "E-mail :",
+ "description": "CommentForm: email label"
},
"C/XGkH": {
"defaultMessage": "Échec du chargement.",
"description": "BlogPage: failed to load text"
},
- "CT3ydM": {
- "defaultMessage": "{date} à {time}",
- "description": "Comment: publication date"
- },
- "CWi0go": {
- "defaultMessage": "Crée le :",
- "description": "ProjectSummary: creation date label"
- },
- "CzTbM4": {
- "defaultMessage": "Contact",
- "description": "ContactPage: breadcrumb item"
+ "D8vB38": {
+ "defaultMessage": "Blog",
+ "description": "Layout: main nav - blog link"
},
- "Dhow1m": {
- "defaultMessage": "Diaspora",
- "description": "Sharing: Diaspora"
+ "DVBwfu": {
+ "defaultMessage": "Souhaitez-vous effectuer une nouvelle recherche ?",
+ "description": "NoResults: try a new search message"
},
"Dq6+WH": {
"defaultMessage": "Thématiques",
"description": "SearchPage: thematics list widget title"
},
- "Enij19": {
- "defaultMessage": "Accueil",
- "description": "Breadcrumb: Home item"
+ "DssFG1": {
+ "defaultMessage": "Dépôts :",
+ "description": "Meta: repositories label"
},
- "EvODgw": {
- "defaultMessage": "Publié en",
- "description": "PostsList: published on year label"
+ "EbFvsM": {
+ "defaultMessage": "Temps de lecture:",
+ "description": "Meta: reading time label"
+ },
+ "Es52wh": {
+ "defaultMessage": "Blog",
+ "description": "Breadcrumb: blog label"
},
"F1EQX3": {
"defaultMessage": "Suivi Ackee (analytique)",
"description": "AckeeSelect: tooltip title"
},
- "F7QxJH": {
- "defaultMessage": "Nom",
- "description": "CommentForm: Name field label"
+ "G+Twgm": {
+ "defaultMessage": "Recherche",
+ "description": "SearchModal: modal title"
},
- "FLkF2R": {
- "defaultMessage": "Tous les articles dans {name}",
- "description": "TopicPage: posts list title"
+ "GRyyfy": {
+ "defaultMessage": "Site officiel:",
+ "description": "Meta: official website label"
},
- "GgIWnN": {
- "defaultMessage": "<a11y>Atteindre </a11y>{title}",
- "description": "ToC: link"
+ "GTbGMy": {
+ "defaultMessage": "Ouvrir le menu",
+ "description": "MainNav: Open label"
},
- "H7C5Bk": {
- "defaultMessage": "Principal",
- "description": "MainNav: aria-label"
+ "GVpTIl": {
+ "defaultMessage": "Sujets",
+ "description": "Error404Page: topics list widget title"
},
- "HEJ3Gv": {
- "defaultMessage": "En cours d'envoi...",
- "description": "CommentForm: submitting message"
+ "Gnf1Si": {
+ "defaultMessage": "{starsCount, plural, =0 {0 étoile sur Github} one {# étoile sur Github} other {# étoiles sur Github}}",
+ "description": "Projets: Github stars count"
},
- "HTdaZj": {
- "defaultMessage": "Pied de page",
- "description": "FooterNav: aria-label"
+ "HFdzae": {
+ "defaultMessage": "Formulaire de contact",
+ "description": "ContactForm: form accessible name"
+ },
+ "HohQPh": {
+ "defaultMessage": "Thématiques",
+ "description": "Error404Page: thematics list widget title"
},
"HriY57": {
"defaultMessage": "Thématiques",
"description": "BlogPage: thematics list widget title"
},
- "ILRLTq": {
- "defaultMessage": "Image de {brandingName}",
- "description": "Branding: branding name picture."
- },
- "IPs/Ck": {
- "defaultMessage": "Twitter",
- "description": "SocialMedia: Twitter"
- },
- "Igx3qp": {
- "defaultMessage": "Projets",
- "description": "Breadcrumb: Projects item"
+ "IY5ew6": {
+ "defaultMessage": "En cours d'envoi...",
+ "description": "CommentForm: spinner message on submit"
},
- "J4nhm4": {
- "defaultMessage": "Commentaire",
- "description": "CommentForm: Comment field label"
+ "JpC3JH": {
+ "defaultMessage": "Autres sujets",
+ "description": "TopicPage: other topics list widget title"
},
- "JeYOeA": {
- "defaultMessage": "Barre latérale",
- "description": "ArticlePage: right sidebar aria-label"
+ "K4rYdT": {
+ "defaultMessage": "Aller au contenu",
+ "description": "Layout: Skip to content link"
},
- "JsOoAW": {
- "defaultMessage": "Site web :",
- "description": "Website: website meta label"
+ "KUowUk": {
+ "defaultMessage": "CV d'{name}",
+ "description": "CVPage: CV image alternative text"
},
- "KERk7L": {
- "defaultMessage": "Filtrer par :",
- "description": "BlogPage: sidebar title"
+ "KVSWGP": {
+ "defaultMessage": "Autres thématiques",
+ "description": "ThematicPage: other thematics list widget title"
},
- "KeRtm/": {
- "defaultMessage": "Thème clair",
- "description": "Icons: Sun icon (light theme)"
+ "KnWeKh": {
+ "defaultMessage": "Page non trouvée",
+ "description": "Error404Page: page title"
},
- "Kqq2cm": {
- "defaultMessage": "En afficher plus ?",
- "description": "BlogPage: load more text"
+ "LCorTC": {
+ "defaultMessage": "Annuler la réponse",
+ "description": "Comment: cancel reply button"
},
- "LR70nt": {
- "defaultMessage": "Sans Javascript, certaines fonctionnalités peuvent ne pas marcher comme charger plus d'articles ou utiliser la recherche. Si vous souhaitez bénéficier de ces fonctionnalités, veuillez activer Javascript.",
- "description": "Layout: noscript banner"
+ "LDDUNO": {
+ "defaultMessage": "Fermer la recherche",
+ "description": "Search: Close label"
},
- "Mj2BQf": {
- "defaultMessage": "CV d'{name}",
- "description": "CVPage: page title"
+ "LszkU6": {
+ "defaultMessage": "Tous les articles dans {thematicName}",
+ "description": "ThematicPage: posts list heading"
},
"N44SOc": {
"defaultMessage": "Projets",
@@ -267,97 +259,61 @@
"defaultMessage": "Sujets",
"description": "SearchPage: topics list widget title"
},
- "Ns8CFb": {
- "defaultMessage": "Commentaires",
- "description": "CommentsList: Comments section title"
- },
- "O9XLDc": {
- "defaultMessage": "Thème :",
- "description": "ThemeToggle: toggle label"
- },
- "OIffB4": {
- "defaultMessage": "Contacter {websiteName} à travers son site. Il vous suffit de remplir le formulaire de contact.",
- "description": "ContactPage: SEO - Meta description"
+ "OF5cPz": {
+ "defaultMessage": "{postsCount, plural, =0 {0 article} one {# article} other {# articles}}",
+ "description": "BlogPage: posts count meta"
},
- "OTTv+m": {
- "defaultMessage": "{count, plural, =0 {Dépôts :} one {Dépôt :} other {Dépôts :}}",
- "description": "ProjectSummary: repositories list label"
+ "OI0N37": {
+ "defaultMessage": "Écrit par :",
+ "description": "Meta: author label"
},
- "OV9r1K": {
- "defaultMessage": "Copié !",
- "description": "Prism: copy button text (clicked)"
+ "OL0Yzx": {
+ "defaultMessage": "Publier",
+ "description": "CommentForm: submit button"
},
- "OccTWi": {
- "defaultMessage": "Page non trouvée",
- "description": "404Page: page title"
+ "OevMeU": {
+ "defaultMessage": "{minutesCount} minutes {secondsCount} secondes",
+ "description": "useReadingTime: minutes + seconds count"
},
"Ogccx6": {
"defaultMessage": "Complet inclut toutes les informations de Partiel ainsi que des informations à propos du site référent, du système d'exploitation, de l'appareil, du navigateur, de la taille d'écran et de la langue.",
"description": "AckeeSelect: tooltip message"
},
- "Oim3rQ": {
- "defaultMessage": "Email",
- "description": "CommentForm: Email field label"
- },
- "P0I+Xm": {
- "defaultMessage": "Journal du hacker",
- "description": "Sharing: Journal du hacker"
- },
- "P7fxX2": {
- "defaultMessage": "Tous les articles dans {name}",
- "description": "ThematicPage: posts list title"
- },
"PXp2hv": {
"defaultMessage": "{websiteName} | Intégrateur web - Développeur WordPress / React",
"description": "HomePage: SEO - Page title"
},
- "PrIz5o": {
- "defaultMessage": "Rechercher un article sur {websiteName}",
- "description": "SearchPage: meta description without query"
- },
- "PxMDzL": {
- "defaultMessage": "Échec du chargement.",
- "description": "ThematicsList: failed to load text"
- },
"PzRpPw": {
"defaultMessage": "Complet",
"description": "AckeeSelect: full option name"
},
- "QHOm5t": {
- "defaultMessage": "Barre latérale",
- "description": "CVPage: right sidebar aria-label"
+ "Q+1GbT": {
+ "defaultMessage": "Barre latérale de la table des matières",
+ "description": "PageLayout: accessible name for ToC sidebar"
+ },
+ "QCW3cy": {
+ "defaultMessage": "Ouvrir les réglages",
+ "description": "Settings: Open label"
+ },
+ "QGi5uD": {
+ "defaultMessage": "Publié le :",
+ "description": "Meta: publication date label"
+ },
+ "QLisK6": {
+ "defaultMessage": "Thème sombre 🌙",
+ "description": "usePrism: toggle dark theme button text"
},
"Qh2CwH": {
"defaultMessage": "Retrouvez-moi ailleurs",
"description": "ContactPage: social media widget title"
},
- "R0eDmw": {
- "defaultMessage": "Blog",
- "description": "BlogPage: breadcrumb item"
- },
"R4yaW6": {
"defaultMessage": "Page suivante {icon}",
"description": "Pagination: Next page link"
},
- "RZzx/4": {
- "defaultMessage": "Javascript est nécessaire pour utiliser la table des matières.",
- "description": "ToC: noscript tag"
- },
- "Rle+UK": {
- "defaultMessage": "Certains champs requis sont vides. Le commentaire ne peut être envoyé.",
- "description": "CommentForm: missing required fields"
- },
- "SWjj4l": {
- "defaultMessage": "Github",
- "description": "SocialMedia: Github"
- },
- "SWq8a4": {
- "defaultMessage": "Fermer {type}",
- "description": "ButtonToolbar: Close button"
- },
- "SX1z3t": {
- "defaultMessage": "Projets : réalisations open-source - {websiteName}",
- "description": "ProjectsPage: SEO - Page title"
+ "R895yC": {
+ "defaultMessage": "CV",
+ "description": "Layout: main nav - cv link"
},
"T4YA64": {
"defaultMessage": "Vous abonner",
@@ -367,305 +323,225 @@
"defaultMessage": "<a11y>Page </a11y>{number}",
"description": "Pagination: page number"
},
- "TfU6Qm": {
- "defaultMessage": "Recherche",
- "description": "SearchPage: breadcrumb item"
- },
- "U+35YD": {
- "defaultMessage": "Recherche",
- "description": "SearchPage: page title"
- },
- "Ua2g2p": {
- "defaultMessage": "Thème clair 🌞",
- "description": "Prism: toggle light theme button text"
+ "TpyFZ6": {
+ "defaultMessage": "Une erreur est survenue :",
+ "description": "Contact: error message"
},
- "Ul2NIl": {
- "defaultMessage": "Merci pour votre commentaire ! Il est maintenant en attente de modération.",
- "description": "CommentForm: success notice but awaiting moderation"
+ "UX9Bu8": {
+ "defaultMessage": "Plier",
+ "description": "HeadingButton: title prefix (expanded state)"
},
"UsQske": {
"defaultMessage": "En lire plus ici :",
"description": "Sharing: content link prefix"
},
- "VSGuGE": {
- "defaultMessage": "Résultats de recherche pour {query}",
- "description": "SearchPage: search results text"
- },
- "VbcHZ4": {
- "defaultMessage": "LinkedIn",
- "description": "SocialMedia: LinkedIn"
- },
- "Vuryko": {
- "defaultMessage": "Email",
- "description": "ContactForm: email field label"
- },
- "WGFOmA": {
+ "VkAnvv": {
"defaultMessage": "Envoyer",
- "description": "CommentForm: Send button"
+ "description": "ContactForm: send button"
},
- "WRkY1/": {
- "defaultMessage": "Replier",
- "description": "ExpandableWidget: collapse text"
+ "Vmj5cw": {
+ "defaultMessage": "Il est maintenant en attente de modération.",
+ "description": "PageLayout: comment awaiting moderation"
},
- "WjVBnY": {
- "defaultMessage": "Twitter",
- "description": "Sharing: Twitter"
- },
- "WpycgB": {
- "defaultMessage": "Attention : le mail n'a pas été envoyé. Certains champs requis sont vides.",
- "description": "ContactForm: missing fields message."
- },
- "X3PDXO": {
- "defaultMessage": "Animations :",
- "description": "ReduceMotion: toggle label"
- },
- "X7n7N2": {
- "defaultMessage": "Envoyer",
- "description": "ContactForm: send button text"
+ "WDwNDl": {
+ "defaultMessage": "Recherche",
+ "description": "SearchPage: SEO - Page title"
},
- "Y1ZdJ6": {
- "defaultMessage": "CV Intégrateur web / Développeur front-end - {websiteName}",
- "description": "CVPage: SEO - Page title"
+ "WKG9wj": {
+ "defaultMessage": "Table des matières",
+ "description": "TableOfContents: the widget title"
},
- "Y3qRib": {
- "defaultMessage": "Formulaire de contact - {websiteName}",
- "description": "ContactPage: SEO - Page title"
+ "WMqQrv": {
+ "defaultMessage": "Rechercher",
+ "description": "SearchForm: button accessible name"
},
- "YEudoh": {
- "defaultMessage": "Lire plus d'articles à propos de :",
- "description": "PostFooter: read more posts about given subjects"
+ "X8oujO": {
+ "defaultMessage": "Rechercher :",
+ "description": "SearchForm: field accessible label"
},
- "YvMPuD": {
- "defaultMessage": "Mots-clés :",
- "description": "SearchForm: search field label"
+ "XKy7rx": {
+ "defaultMessage": "Vous pouvez également tenter une recherche :",
+ "description": "Error404Page: try a search message"
},
- "YwvYfw": {
- "defaultMessage": "Table des matières",
- "description": "ThematicPage: ToC sidebar aria-label"
+ "Xj+WXB": {
+ "defaultMessage": "Ouvrir la recherche",
+ "description": "Search: Open label"
},
- "Z1eSIz": {
- "defaultMessage": "Ouvrez {type}",
- "description": "ButtonToolbar: Open button"
- },
- "ZJMNRW": {
- "defaultMessage": "Accueil",
- "description": "MainNav: home link"
+ "Ygea7s": {
+ "defaultMessage": "Thème clair",
+ "description": "ThemeToggle: light theme label"
},
- "ZWh78Y": {
- "defaultMessage": "Désolé, il semble que la page demandée n'existe pas. Si vous pensez que le chemin devrait exister, n'hésitez pas à <link>me contacter</link> avec les informations nécessaires pour que je puisse corriger le problème.",
- "description": "404Page: page body"
+ "ZIrTee": {
+ "defaultMessage": "Nom :",
+ "description": "CommentForm: name label"
},
- "Zg4L7U": {
- "defaultMessage": "Table des matières",
- "description": "ToC: widget title"
+ "ZNBhDP": {
+ "defaultMessage": "Résultats de la recherche pour {query}",
+ "description": "SearchPage: SEO - Page title"
},
- "Zlkww3": {
- "defaultMessage": "Échec du chargement.",
- "description": "CommentsList: failed to load"
+ "Zpgv+f": {
+ "defaultMessage": "En lire plus<a11y> à propos de {title}</a11y>",
+ "description": "Summary: read more link"
},
- "aA3hOT": {
- "defaultMessage": "{starsCount, plural, =0 {0 étoile sur Github} one {# étoile sur Github} other {# étoiles sur Github}}",
- "description": "ProjectSummary: technologies list label"
+ "aJC7D2": {
+ "defaultMessage": "Fermer le menu",
+ "description": "MainNav: Close label"
},
"aMFqPH": {
"defaultMessage": "{icon} Page précédente",
"description": "Pagination: previous page link"
},
- "akSutM": {
- "defaultMessage": "Projets",
- "description": "MainNav: projects link"
+ "azgQuH": {
+ "defaultMessage": "Vous devriez lire {title}",
+ "description": "Sharing: subject text"
},
- "azc1GT": {
- "defaultMessage": "Ouvrir le menu",
- "description": "MainNav: open button"
- },
- "bBdMGm": {
- "defaultMessage": "Découvrez le CV d'{websiteName}, intégrateur web / développeur front-end en France : compétences, expériences et formations.",
- "description": "CVPage: SEO - Meta description"
+ "b4fdYE": {
+ "defaultMessage": "Créé le :",
+ "description": "Meta: creation date label"
},
- "bHEmkY": {
- "defaultMessage": "Réglages",
- "description": "Settings: modal title"
+ "bcyOgC": {
+ "defaultMessage": "Déplier",
+ "description": "HeadingButton: title prefix (collapsed state)"
},
- "bkbrN7": {
- "defaultMessage": "En lire plus<a11y> à propos de {title}</a11y>",
- "description": "PostPreview: read more link"
+ "bojYF5": {
+ "defaultMessage": "Accueil",
+ "description": "Layout: main nav - home link"
},
- "c2NtPj": {
- "defaultMessage": "Contact",
- "description": "MainNav: contact link"
+ "bz53Us": {
+ "defaultMessage": "Thématiques:",
+ "description": "Meta: thematics label"
},
- "cjK9Ad": {
- "defaultMessage": "Une erreur inattendue est survenue. Le commentaire ne peut pas être envoyé.",
- "description": "CommentForm: error notice"
+ "c556Qo": {
+ "defaultMessage": "Barre latérale",
+ "description": "PageLayout: accessible name for the sidebar"
},
- "csCQQk": {
- "defaultMessage": "LinkedIn",
- "description": "Sharing: LinkedIn"
+ "cl7YNU": {
+ "defaultMessage": "CC BY SA",
+ "description": "CCBySA: icon title"
},
- "dE8xxV": {
- "defaultMessage": "Fermer le menu",
- "description": "MainNav: close button"
+ "d4N8nD": {
+ "defaultMessage": "Pied de page",
+ "description": "Footer: an accessible name for footer nav"
},
- "dqrd6I": {
- "defaultMessage": "Retour en haut",
- "description": "Footer: Back to top button"
+ "dDK5oc": {
+ "defaultMessage": "Photo d'{website}",
+ "description": "Branding: photo alternative text"
},
- "du4MLN": {
- "defaultMessage": "{articlesCount, plural, =0 {# article chargé} one {# article chargé} other {# articles chargés}} sur un total de {total}",
- "description": "PaginationCursor: loaded articles count message"
+ "dz2kDV": {
+ "defaultMessage": "Formulaire des commentaires",
+ "description": "CommentForm: aria label"
},
"e/8Kyj": {
"defaultMessage": "Partiel",
"description": "AckeeSelect: partial option name"
},
- "e1Forh": {
- "defaultMessage": "Annuler la réponse",
- "description": "Comment: reply button"
- },
- "e9L59q": {
- "defaultMessage": "Aucun commentaire.",
- "description": "CommentsList: No comment message"
- },
- "eFMu2E": {
- "defaultMessage": "Rechercher",
- "description": "SearchForm : form title"
- },
- "eUXMG4": {
- "defaultMessage": "Vu sur {domainName} :",
- "description": "Sharing: seen on text"
- },
- "enwhNm": {
- "defaultMessage": "{count, plural, =0 {Technologies :} one {Technologie :} other {Technologies :}}",
- "description": "ProjectSummary: technologies list label"
- },
- "eu3beS": {
- "defaultMessage": "Barre latérale",
- "description": "TopicPage: right sidebar aria-label"
+ "fN04AJ": {
+ "defaultMessage": "<link>Télécharger le CV au format PDF</link>",
+ "description": "CVPage: download CV in PDF text"
},
"fOe8rH": {
"defaultMessage": "Échec du chargement.",
"description": "SearchPage: failed to load text"
},
- "g1cFCa": {
- "defaultMessage": "Javascript est nécessaire pour poster un commentaire.",
- "description": "CommentForm: noscript tag"
+ "fcHeyC": {
+ "defaultMessage": "{date} à {time}",
+ "description": "Meta: publication date and time"
+ },
+ "fkcTGp": {
+ "defaultMessage": "Une erreur est survenue:",
+ "description": "PageLayout: comment form error message"
},
- "g4DckL": {
- "defaultMessage": "Table des matières",
- "description": "CVPage: ToC sidebar aria-label"
+ "ftXN+0": {
+ "defaultMessage": "Blocs de code :",
+ "description": "PrismThemeToggle: theme label"
},
- "gQKeF+": {
- "defaultMessage": "Merci. Votre message a bien été envoyé. J'y répondrai dès que possible.",
- "description": "ContactForm: success message"
+ "g3+Ahv": {
+ "defaultMessage": "Il a été approuvé.",
+ "description": "PageLayout: comment approved."
},
- "hHrNd0": {
- "defaultMessage": "Barre latérale",
- "description": "ProjectPage: right sidebar aria-label"
+ "gJNaBD": {
+ "defaultMessage": "Sujets :",
+ "description": "Meta: topics label"
},
- "hKagVG": {
- "defaultMessage": "Licence :",
- "description": "ProjectSummary: license label"
+ "gPfT/K": {
+ "defaultMessage": "Réglages",
+ "description": "SettingsModal: title"
},
- "hV0qHp": {
- "defaultMessage": "Déplier",
- "description": "ExpandableWidget: expand text"
+ "gX+YVy": {
+ "defaultMessage": "Formulaire des réglages",
+ "description": "SettingsForm: an accessible form name"
+ },
+ "hHVgW3": {
+ "defaultMessage": "Thème clair 🌞",
+ "description": "usePrism: toggle light theme button text"
},
"hzHuCc": {
"defaultMessage": "Répondre",
"description": "Comment: reply button"
},
+ "i+/ckF": {
+ "defaultMessage": "Aide",
+ "description": "HelpButton: screen reader text"
+ },
"i5L19t": {
"defaultMessage": "Shaarli",
"description": "HomePage: link to Shaarli"
},
- "iqAbyn": {
- "defaultMessage": "Aller au contenu",
- "description": "Layout: Skip to content button"
+ "i7Wq3G": {
+ "defaultMessage": "{count} secondes",
+ "description": "useReadingTime: seconds count"
},
- "iyEh0R": {
- "defaultMessage": "Échec du chargement.",
- "description": "RecentPosts: failed to load text"
+ "j5k9Fe": {
+ "defaultMessage": "Accueil",
+ "description": "Breadcrumb: home label"
},
"jASD7k": {
"defaultMessage": "Linux",
"description": "HomePage: link to Linux thematic"
},
- "jCyqZS": {
- "defaultMessage": "Écrit par :",
- "description": "Author: article author meta label"
- },
- "jN+dY5": {
- "defaultMessage": "Site web",
- "description": "CommentForm: Website field label"
- },
- "jpv+Nz": {
- "defaultMessage": "CV",
- "description": "MainNav: resume link"
- },
- "k7/SkN": {
- "defaultMessage": "Environ {number} mots",
- "description": "ReadingTime: number of words"
- },
- "lKGNKx": {
- "defaultMessage": "{total, plural, =0 {Aucun commentaire} one {# commentaire} other {# commentaires}}",
- "description": "CommentsCount: comment count value"
+ "jTVIh8": {
+ "defaultMessage": "Commentaires:",
+ "description": "Meta: comments label"
},
- "lKZm9t": {
- "defaultMessage": "Email",
- "description": "Sharing: Email"
- },
- "lsDB5G": {
- "defaultMessage": "Table des matières",
- "description": "TopicPage: ToC sidebar aria-label"
- },
- "mC21ht": {
- "defaultMessage": "Nombre d'articles chargés sur le total disponible.",
- "description": "PaginationCursor: loaded articles count aria-label"
- },
- "mh7tGg": {
- "defaultMessage": "Aperçu de {title}",
- "description": "ProjectSummary: cover alt text"
+ "kzIYoQ": {
+ "defaultMessage": "Laisser un commentaire",
+ "description": "PageLayout: comment form title"
},
- "n0Gbod": {
- "defaultMessage": "Temps de lecture :",
- "description": "ReadingTime: reading time meta label"
+ "lKhTGM": {
+ "defaultMessage": "Utilisez Ctrl+c pour copier",
+ "description": "usePrism: copy button error text"
},
- "nFMdWI": {
- "defaultMessage": "Thème sombre 🌙",
- "description": "Prism: toggle dark theme button text"
+ "m+SUSR": {
+ "defaultMessage": "Retour en haut de page",
+ "description": "BackToTop: link text"
},
- "norrGp": {
- "defaultMessage": "Autres thématiques",
- "description": "ThematicPage: thematics list widget title"
+ "npisb3": {
+ "defaultMessage": "Rechercher un article sur {websiteName}.",
+ "description": "SearchPage: SEO - Meta description"
},
- "oPf+XA": {
- "defaultMessage": "Aide",
- "description": "ButtonHelp: screen reader text"
+ "nsw6Th": {
+ "defaultMessage": "Copié !",
+ "description": "usePrism: copy button text (clicked)"
},
- "obmlFh": {
- "defaultMessage": "Gitlab",
- "description": "SocialMedia: Gitlab"
+ "nwbzKm": {
+ "defaultMessage": "Mentions légales",
+ "description": "Layout: Legal notice label"
},
- "ode0YK": {
+ "og/zWL": {
"defaultMessage": "Thème sombre",
- "description": "Icons: Moon icon (dark theme)"
- },
- "okFrAO": {
- "defaultMessage": "{count, plural, =0 {Technologies :} one {Technologie :} other {Technologies :}}",
- "description": "ProjectPreview: technologies list label"
+ "description": "PrismThemeToggle: dark theme label"
},
- "p1zZ/Z": {
- "defaultMessage": "Total :",
- "description": "PostCount: total found articles meta label"
+ "pWKyyR": {
+ "defaultMessage": "Arrêt",
+ "description": "MotionToggle: deactivate reduce motion label"
},
- "pEtJik": {
- "defaultMessage": "En charger plus ?",
- "description": "SearchPage: load more text"
+ "pWTj2W": {
+ "defaultMessage": "Popularité :",
+ "description": "Meta: popularity label"
},
- "pTxT7N": {
- "defaultMessage": "Une erreur est survenue :",
- "description": "ContactForm: error message"
+ "pg26sn": {
+ "defaultMessage": "Découvrez les résultats de recherche pour {query} sur {websiteName}.",
+ "description": "SearchPage: SEO - Meta description"
},
"q3U6uI": {
"defaultMessage": "Partager",
@@ -675,21 +551,21 @@
"defaultMessage": "Chargement...",
"description": "Spinner: loading text"
},
- "qPU/Qn": {
- "defaultMessage": "Marche",
- "description": "ReduceMotion: toggle on label"
+ "qnwsWV": {
+ "defaultMessage": "Projets",
+ "description": "Layout: main nav - projects link"
},
- "qXQETZ": {
- "defaultMessage": "{thematicsCount, plural, =0 {Thématiques liées} one {Thématique liée} other {Thématiques liées}}",
- "description": "RelatedThematics: widget title"
+ "s1i43J": {
+ "defaultMessage": "{minutesCount} minutes",
+ "description": "useReadingTime: rounded minutes count"
},
- "rXeTkM": {
- "defaultMessage": "Ce commentaire est en attente de modération.",
- "description": "Comment: awaiting moderation message"
+ "s8/tyz": {
+ "defaultMessage": "Sujet :",
+ "description": "ContactForm: object label"
},
- "s6U1Xt": {
- "defaultMessage": "Découvrez les projets d'{websiteName} qui sont essentiellement liés au développement web et au libre..",
- "description": "ProjectsPage: SEO - Meta description"
+ "sBwfCy": {
+ "defaultMessage": "Aucun commentaire.",
+ "description": "PageLayout: no comments text"
},
"sO/Iwj": {
"defaultMessage": "Me contacter",
@@ -699,100 +575,76 @@
"defaultMessage": "Partiel inclut seulement l'url de la page, le nombre de visites et la durée.",
"description": "AckeeSelect: tooltip message"
},
- "soj7do": {
- "defaultMessage": "Publié le :",
- "description": "Comment: publication date label"
+ "suXOBu": {
+ "defaultMessage": "Thème :",
+ "description": "ThemeToggle: theme label"
},
- "syLgY9": {
- "defaultMessage": "Barre latérale",
- "description": "ThematicPage: right sidebar aria-label"
+ "tLC7bh": {
+ "defaultMessage": "Mis à jour le :",
+ "description": "Meta: update date label"
},
"tMuNTy": {
"defaultMessage": "{websiteName} est intégrateur web / développeur front-end en France. Il code et il écrit essentiellement à propos de développement web et du libre.",
"description": "HomePage: SEO - Meta description"
},
- "txusHd": {
- "defaultMessage": "Tous les champs marqués avec * sont requis.",
- "description": "ContactPage: required fields text"
+ "tsWh8x": {
+ "defaultMessage": "Thème clair",
+ "description": "PrismThemeToggle: light theme label"
},
- "uMURuJ": {
- "defaultMessage": "Sujet",
- "description": "ContactForm: subject field label"
+ "u41qSk": {
+ "defaultMessage": "Site web :",
+ "description": "CommentForm: website label"
+ },
+ "uaqd5F": {
+ "defaultMessage": "Charger plus d'articles ?",
+ "description": "PostsList: load more button"
},
"ureXFw": {
"defaultMessage": "Partager via {name}",
"description": "Sharing: share on social network text"
},
- "uvB+32": {
- "defaultMessage": "Découvrez les mentions légales du site d'{websiteName}.",
- "description": "LegalNoticePage: SEO - Meta description"
- },
- "vJ+QDV": {
- "defaultMessage": "Dernière mise à jour le :",
- "description": "ProjectSummary: update date label"
- },
- "vK7Sxv": {
- "defaultMessage": "Aucun résultat.",
- "description": "PostsList: no results"
- },
- "vgMk0q": {
- "defaultMessage": "Popularité :",
- "description": "ProjectSummary: popularity label"
+ "va65iw": {
+ "defaultMessage": "Marche",
+ "description": "MotionToggle: activate reduce motion label"
},
"vkF/RP": {
"defaultMessage": "Développement web",
"description": "HomePage: link to web development thematic"
},
- "w/lPUh": {
- "defaultMessage": "{topicsCount, plural, =0 {Sujets liés} one {Sujet lié} other {Sujets liés}}",
- "description": "RelatedTopics: widget title"
- },
- "w0UfY0": {
- "defaultMessage": "Blocs de code :",
- "description": "PrismThemeToggle: toggle label"
- },
- "w1nIrj": {
- "defaultMessage": "Arrêt",
- "description": "ReduceMotion: toggle off label"
+ "w4B5PA": {
+ "defaultMessage": "E-mail :",
+ "description": "ContactForm: email label"
},
"w8GrOf": {
"defaultMessage": "Libre",
"description": "HomePage: link to free thematic"
},
- "wdqOpf": {
- "defaultMessage": "{time, plural, =0 {# minute} one {# minute} other {# minutes}}",
- "description": "ReadingTime: reading time value"
+ "x55qsD": {
+ "defaultMessage": "Logo d'{website}",
+ "description": "Branding: logo title"
},
- "xC3Khf": {
- "defaultMessage": "Télécharger le <link>CV au format PDF</link>",
- "description": "CVPreview: download as PDF link"
+ "xaqaYQ": {
+ "defaultMessage": "Mail en cours d'envoi...",
+ "description": "ContactForm: spinner message on submit"
},
- "ySsWZl": {
- "defaultMessage": "moins d'une minute",
- "description": "ReadingTime: Reading time value"
+ "yE/Jdz": {
+ "defaultMessage": "Vous êtes ici :",
+ "description": "Pagination: current page indication"
},
- "yWjXRx": {
- "defaultMessage": "Mentions légales",
- "description": "FooterNav: legal notice link"
+ "yN5P+m": {
+ "defaultMessage": "Message :",
+ "description": "ContactForm: message label"
},
"yfgMcl": {
"defaultMessage": "Introduction :",
"description": "Sharing: email content prefix"
},
- "ywkCsK": {
- "defaultMessage": "Erreur 404",
- "description": "404Page: breadcrumb item"
- },
- "z0ic9c": {
- "defaultMessage": "Blog",
- "description": "Breadcrumb: Blog item"
- },
- "z9qkcQ": {
- "defaultMessage": "Utilisez Ctrl+c pour copier",
- "description": "Prism: error text"
+ "zEN3fd": {
+ "defaultMessage": "Tous les articles dans {topicName}",
+ "description": "TopicPage: posts list heading"
},
- "zPJifH": {
- "defaultMessage": "Blog",
- "description": "MainNav: blog link"
+ "zbzlb1": {
+ "defaultMessage": "Page {number}",
+ "description": "BlogPage: page number"
}
}
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
index 24c6951..c3a5cac 100644
--- a/src/pages/404.tsx
+++ b/src/pages/404.tsx
@@ -1,30 +1,89 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Link from '@components/atoms/links/link';
+import SearchForm from '@components/organisms/forms/search-form';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import {
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+} from '@utils/helpers/pages';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
-import Link from 'next/link';
-import { FormattedMessage, useIntl } from 'react-intl';
+import { ReactNode } from 'react';
+import { useIntl } from 'react-intl';
-const Error404: NextPageWithLayout = () => {
- const intl = useIntl();
+type Error404PageProps = {
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ translation: Messages;
+};
+/**
+ * Error 404 page.
+ */
+const Error404Page: NextPageWithLayout<Error404PageProps> = ({
+ thematicsList,
+ topicsList,
+}) => {
+ const intl = useIntl();
+ const { website } = useSettings();
+ const title = intl.formatMessage({
+ defaultMessage: 'Page not found',
+ description: 'Error404Page: page title',
+ id: 'KnWeKh',
+ });
+ const body = intl.formatMessage(
+ {
+ defaultMessage:
+ 'Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem.',
+ id: '9sGNKq',
+ description: 'Error404Page: page body',
+ },
+ {
+ link: (chunks: ReactNode) => <Link href="/contact">{chunks}</Link>,
+ }
+ );
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/404`,
+ });
const pageTitle = intl.formatMessage(
{
defaultMessage: 'Error 404: Page not found - {websiteName}',
description: '404Page: SEO - Page title',
id: '310o3F',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
const pageDescription = intl.formatMessage({
defaultMessage: 'Page not found.',
description: '404Page: SEO - Meta description',
id: '48Ww//',
});
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'Error404Page: thematics list widget title',
+ id: 'HohQPh',
+ });
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'Error404Page: topics list widget title',
+ id: 'GVpTIl',
+ });
return (
<>
@@ -32,54 +91,64 @@ const Error404: NextPageWithLayout = () => {
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
</Head>
- <div className={`${styles.article} ${styles['article--no-comments']}`}>
- <PostHeader
- title={intl.formatMessage({
- defaultMessage: 'Page not found',
- description: '404Page: page title',
- id: 'OccTWi',
+ <PageLayout
+ title={title}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
+ >
+ {body}
+ <p>
+ {intl.formatMessage({
+ defaultMessage: 'You can also try a search:',
+ description: 'Error404Page: try a search message',
+ id: 'XKy7rx',
})}
- />
- <div className={styles.body}>
- <FormattedMessage
- defaultMessage="Sorry, it seems that the page your are looking for does not exist. If you think this path should work, feel free to <link>contact me</link> with the necessary information so that I can fix the problem."
- description="404Page: page body"
- id="ZWh78Y"
- values={{
- link: (chunks: string) => (
- <Link href="/contact/">
- <a>{chunks}</a>
- </Link>
- ),
- }}
- />
- </div>
- </div>
+ </p>
+ <SearchForm hideLabel={true} searchPage="/recherche/" />
+ </PageLayout>
</>
);
};
-Error404.getLayout = getLayout;
+Error404Page.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Error 404',
- description: '404Page: breadcrumb item',
- id: 'ywkCsK',
- });
- const { locale } = context;
+export const getStaticProps: GetStaticProps<Error404PageProps> = async ({
+ locale,
+}) => {
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
translation,
},
};
};
-export default Error404;
+export default Error404Page;
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 84c2469..5bc9f85 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -1,4 +1,4 @@
-import { AppPropsWithLayout } from '@ts/types/app';
+import { type AppPropsWithLayout } from '@ts/types/app';
import { settings } from '@utils/config';
import { AckeeProvider } from '@utils/providers/ackee';
import { PrismThemeProvider } from '@utils/providers/prism-theme';
@@ -7,11 +7,11 @@ import { useRouter } from 'next/router';
import { IntlProvider } from 'react-intl';
import '../styles/globals.scss';
-const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
+const App = ({ Component, pageProps }: AppPropsWithLayout) => {
const { locale, defaultLocale } = useRouter();
const appLocale: string = locale || settings.locales.defaultLocale;
-
const getLayout = Component.getLayout ?? ((page) => page);
+
return (
<AckeeProvider domain={settings.ackee.url} siteId={settings.ackee.siteId}>
<IntlProvider
@@ -25,7 +25,7 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
enableSystem={true}
>
<PrismThemeProvider>
- {getLayout(<Component {...pageProps} />)}
+ {getLayout(<Component {...pageProps} />, {})}
</PrismThemeProvider>
</ThemeProvider>
</IntlProvider>
@@ -33,4 +33,4 @@ const MyApp = ({ Component, pageProps }: AppPropsWithLayout) => {
);
};
-export default MyApp;
+export default App;
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index 27a6f7b..ea679ab 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -1,286 +1,262 @@
-import CommentForm from '@components/CommentForm/CommentForm';
-import CommentsList from '@components/CommentsList/CommentsList';
-import { getLayout } from '@components/Layouts/Layout';
-import PostFooter from '@components/PostFooter/PostFooter';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { Sharing, ToC } from '@components/Widgets';
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Link from '@components/atoms/links/link';
+import Spinner from '@components/atoms/loaders/spinner';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import Sharing from '@components/organisms/widgets/sharing';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
import {
- getAllPostsSlug,
- getCommentsByPostId,
- getPostBySlug,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta, ArticleProps } from '@ts/types/articles';
-import { PrismDefaultPlugins, PrismPlugins } from '@ts/types/prism';
-import { settings } from '@utils/config';
-import { getFormattedPaths } from '@utils/helpers/format';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { addPrismClasses } from '@utils/helpers/prism';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+ getAllArticlesSlugs,
+ getArticleBySlug,
+} from '@services/graphql/articles';
+import { getPostComments } from '@services/graphql/comments';
+import styles from '@styles/pages/article.module.scss';
+import {
+ type Article,
+ type Comment,
+ type NextPageWithLayout,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import usePrism, { type OptionalPrismPlugin } from '@utils/hooks/use-prism';
+import useReadingTime from '@utils/hooks/use-reading-time';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
-import Prism from 'prismjs';
import { ParsedUrlQuery } from 'querystring';
-import { useCallback, useEffect, useMemo } from 'react';
+import { HTMLAttributes } from 'react';
import { useIntl } from 'react-intl';
-import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts';
+import useSWR from 'swr';
+
+type ArticlePageProps = {
+ comments: Comment[];
+ post: Article;
+ slug: string;
+ translation: Messages;
+};
-const SingleArticle: NextPageWithLayout<ArticleProps> = ({
+/**
+ * Article page.
+ */
+const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
comments,
post,
+ slug,
}) => {
+ const { isFallback } = useRouter();
const intl = useIntl();
- const router = useRouter();
+ const { data: article } = useSWR(() => slug, getArticleBySlug, {
+ fallbackData: post,
+ });
+ const { data: commentsData } = useSWR(() => id, getPostComments, {
+ fallbackData: comments,
+ });
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title: article?.title || '',
+ url: `/article/${slug}`,
+ });
+ const readingTime = useReadingTime(article?.meta.wordsCount || 0, true);
+ const { website } = useSettings();
+ const prismPlugins: OptionalPrismPlugin[] = ['command-line', 'line-numbers'];
+ const { attributes, className } = usePrism({ plugins: prismPlugins });
- const loadPrismPlugins = useCallback(
- async (prismPlugins: (PrismDefaultPlugins | PrismPlugins)[]) => {
- for (const plugin of prismPlugins) {
- try {
- if (plugin === 'color-scheme') {
- await import(`@utils/plugins/prism-${plugin}`);
- } else {
- await import(`prismjs/plugins/${plugin}/prism-${plugin}.min.js`);
+ if (isFallback) return <Spinner />;
- if (plugin === 'autoloader')
- Prism.plugins.autoloader.languages_path = '/prism/';
- }
- } catch (error) {
- console.error('Article: an error occurred with Prism.');
- console.error(error);
- }
- }
- },
- []
- );
+ const { content, id, intro, meta, title } = article!;
+ const { author, commentsCount, cover, dates, seo, thematics, topics } = meta;
- const plugins: (PrismDefaultPlugins | PrismPlugins)[] = useMemo(
- () => [
- 'autoloader',
- 'toolbar',
- 'show-language',
- 'copy-to-clipboard',
- 'color-scheme',
- 'command-line',
- 'line-numbers',
- 'match-braces',
- 'normalize-whitespace',
- ],
- []
- );
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ author: author?.name,
+ publication: { date: dates.publication },
+ update:
+ dates.update && dates.publication !== dates.update
+ ? { date: dates.update }
+ : undefined,
+ readingTime,
+ thematics:
+ thematics &&
+ thematics.map((thematic) => (
+ <Link key={thematic.id} href={thematic.url}>
+ {thematic.name}
+ </Link>
+ )),
+ };
- useEffect(() => {
- loadPrismPlugins(plugins).then(() => {
- addPrismClasses();
- Prism.highlightAll();
- });
- }, [plugins, loadPrismPlugins]);
+ const footerMetaLabel = intl.formatMessage({
+ defaultMessage: 'Read more articles about:',
+ description: 'ArticlePage: footer topics list label',
+ id: '50xc4o',
+ });
- if (router.isFallback) return <Spinner />;
+ const footerMeta: PageLayoutProps['footerMeta'] = {
+ custom: topics && {
+ label: footerMetaLabel,
+ value: topics.map((topic) => {
+ return (
+ <ButtonLink key={topic.id} target={topic.url} className={styles.btn}>
+ {topic.logo && <ResponsiveImage {...topic.logo} />} {topic.name}
+ </ButtonLink>
+ );
+ }),
+ },
+ };
- const {
- author,
- commentCount,
+ const webpageSchema = getWebPageSchema({
+ description: intro,
+ locale: website.locales.default,
+ slug,
+ title,
+ updateDate: dates.update,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: true,
+ locale: website.locales.default,
+ slug,
+ });
+ const blogPostSchema = getSinglePageSchema({
+ commentsCount,
content,
- databaseId,
+ cover: cover?.src,
dates,
- featuredImage,
- info,
- intro,
- seo,
- topics,
- thematics,
+ description: intro,
+ id: 'article',
+ kind: 'post',
+ locale: website.locales.default,
+ slug,
title,
- } = post;
-
- const meta: ArticleMeta = {
- author,
- commentCount: commentCount || undefined,
- dates,
- readingTime: info.readingTime,
- thematics,
- wordsCount: info.wordsCount,
- };
-
- const articleUrl = `${settings.url}${router.asPath}`;
+ });
+ const schemaJsonLd = getSchemaJson([
+ webpageSchema,
+ blogSchema,
+ blogPostSchema,
+ ]);
- const webpageSchema: WebPage = {
- '@id': `${articleUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- lastReviewed: dates.update,
- name: seo.title,
- description: seo.metaDesc,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${articleUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ const lineNumbersClassName = className
+ .replace('command-line', '')
+ .replace(/\s\s+/g, ' ');
+ const commandLineClassName = className
+ .replace('line-numbers', '')
+ .replace(/\s\s+/g, ' ');
- const blogSchema: Blog = {
- '@id': `${settings.url}/#blog`,
- '@type': 'Blog',
- blogPost: { '@id': `${settings.url}/#article` },
- isPartOf: {
- '@id': `${articleUrl}`,
- },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- };
+ /**
+ * Replace a string with Prism classnames and attributes.
+ *
+ * @param {string} str - The found string.
+ * @returns {string} The classes and attributes.
+ */
+ const prismClassNameReplacer = (str: string): string => {
+ const wpBlockClassName = 'wp-block-code';
+ const languageArray = str.match(/language-[^\s|"]+/);
+ const languageClassName = languageArray ? `${languageArray[0]}` : '';
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
+ if (
+ str.includes('command-line') ||
+ (!str.includes('command-line') && str.includes('language-bash'))
+ ) {
+ return `class="${wpBlockClassName} ${commandLineClassName}${languageClassName}" tabindex="0" data-filter-output="#output#`;
+ }
- const blogPostSchema: BlogPosting = {
- '@id': `${settings.url}/#article`,
- '@type': 'BlogPosting',
- name: title,
- description: intro,
- articleBody: content,
- author: { '@id': `${settings.url}/#branding` },
- commentCount: commentCount || undefined,
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- discussionUrl: `${articleUrl}/#comments`,
- editor: { '@id': `${settings.url}/#branding` },
- headline: title,
- image: featuredImage?.sourceUrl,
- inLanguage: settings.locales.defaultLocale,
- isPartOf: {
- '@id': `${settings.url}/blog`,
- },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${articleUrl}` },
- thumbnailUrl: featuredImage?.sourceUrl,
+ return `class="${wpBlockClassName} ${lineNumbersClassName}${languageClassName}" tabindex="0`;
};
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, blogSchema, blogPostSchema],
- };
+ const contentWithPrismClasses = content.replaceAll(
+ /class="wp-block-code[^"]+/gm,
+ prismClassNameReplacer
+ );
- const copyText = intl.formatMessage({
- defaultMessage: 'Copy',
- description: 'Prism: copy button text (no clicked)',
- id: '/ly3AC',
- });
- const copiedText = intl.formatMessage({
- defaultMessage: 'Copied!',
- description: 'Prism: copy button text (clicked)',
- id: 'OV9r1K',
- });
- const errorText = intl.formatMessage({
- defaultMessage: 'Use Ctrl+c to copy',
- description: 'Prism: error text',
- id: 'z9qkcQ',
- });
- const darkTheme = intl.formatMessage({
- defaultMessage: 'Dark Theme 🌙',
- description: 'Prism: toggle dark theme button text',
- id: 'nFMdWI',
- });
- const lightTheme = intl.formatMessage({
- defaultMessage: 'Light Theme 🌞',
- description: 'Prism: toggle light theme button text',
- id: 'Ua2g2p',
- });
+ const pageUrl = `${website.url}${slug}`;
return (
<>
<Head>
<title>{seo.title}</title>
- <meta name="description" content={seo.metaDesc} />
- <meta property="og:url" content={`${articleUrl}`} />
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${pageUrl}`} />
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={intro} />
- <meta property="og:image" content={featuredImage?.sourceUrl} />
- <meta property="og:image:alt" content={featuredImage?.altText} />
</Head>
<Script
- id="schema-article"
+ id="schema-project"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="article"
- className={styles.article}
- data-prismjs-copy={copyText}
- data-prismjs-copy-success={copiedText}
- data-prismjs-copy-error={errorText}
- data-prismjs-color-scheme-dark={darkTheme}
- data-prismjs-color-scheme-light={lightTheme}
+ <PageLayout
+ allowComments={true}
+ bodyAttributes={{
+ ...(attributes as HTMLAttributes<HTMLDivElement>),
+ }}
+ bodyClassName={styles.body}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ comments={commentsData}
+ footerMeta={footerMeta}
+ headerMeta={headerMeta}
+ id={id as number}
+ intro={intro}
+ title={title}
+ withToC={true}
+ widgets={[
+ <Sharing
+ key="sharing-widget"
+ className={styles.widget}
+ data={{ excerpt: intro, title, url: pageUrl }}
+ media={[
+ 'diaspora',
+ 'email',
+ 'facebook',
+ 'journal-du-hacker',
+ 'linkedin',
+ 'twitter',
+ ]}
+ />,
+ ]}
>
- <PostHeader intro={intro} meta={meta} title={title} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'ArticlePage: ToC sidebar aria-label',
- id: '9nhYRA',
- })}
- >
- <ToC />
- </Sidebar>
- <div
- className={styles.body}
- dangerouslySetInnerHTML={{ __html: content }}
- ></div>
- <PostFooter topics={topics} />
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'ArticlePage: right sidebar aria-label',
- id: 'JeYOeA',
- })}
- >
- <Sharing title={title} excerpt={intro} />
- </Sidebar>
- <section id="comments" className={styles.comments}>
- <CommentsList articleId={databaseId} comments={comments} />
- <CommentForm articleId={databaseId} />
- </section>
- </article>
+ {contentWithPrismClasses}
+ </PageLayout>
</>
);
};
-SingleArticle.getLayout = getLayout;
+ArticlePage.getLayout = (page) => getLayout(page, { useGrid: true });
interface PostParams extends ParsedUrlQuery {
slug: string;
}
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<ArticlePageProps> = async ({
+ locale,
+ params,
+}) => {
+ const post = await getArticleBySlug(params!.slug as PostParams['slug']);
+ const comments = await getPostComments(post.id as number);
const translation = await loadTranslation(locale);
- const { slug } = context.params as PostParams;
- const post = await getPostBySlug(slug);
- const comments = await getCommentsByPostId(post.databaseId);
- const breadcrumbTitle = post.title;
return {
props: {
- breadcrumbTitle,
- comments,
- post,
+ comments: JSON.parse(JSON.stringify(comments)),
+ post: JSON.parse(JSON.stringify(post)),
+ slug: post.slug,
translation,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
- const allSlugs = await getAllPostsSlug();
- const paths = getFormattedPaths(allSlugs);
+ const slugs = await getAllArticlesSlugs();
+ const paths = slugs.map((slug) => {
+ return { params: { slug } };
+ });
return {
paths,
@@ -288,4 +264,4 @@ export const getStaticPaths: GetStaticPaths = async () => {
};
};
-export default SingleArticle;
+export default ArticlePage;
diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx
index b5ced07..3f7eefd 100644
--- a/src/pages/blog/index.tsx
+++ b/src/pages/blog/index.tsx
@@ -1,116 +1,79 @@
-import { Button } from '@components/Buttons';
-import { getLayout } from '@components/Layouts/Layout';
-import Pagination from '@components/Pagination/Pagination';
-import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostsList from '@components/PostsList/PostsList';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { ThematicsList, TopicsList } from '@components/Widgets';
+import Notice from '@components/atoms/layout/notice';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { type EdgesResponse } from '@services/graphql/api';
+import { getArticles, getTotalArticles } from '@services/graphql/articles';
import {
- getAllThematics,
- getAllTopics,
- getPostsTotal,
- getPublishedPosts,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { BlogPageProps, PostsList as PostsListData } from '@ts/types/blog';
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsList,
+} from '@utils/helpers/pages';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import usePagination from '@utils/hooks/use-pagination';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
-import { useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
-import { Blog as BlogSchema, Graph, WebPage } from 'schema-dts';
-import useSWRInfinite from 'swr/infinite';
-const Blog: NextPageWithLayout<BlogPageProps> = ({
- allThematics,
- allTopics,
- posts,
- totalPosts,
+type BlogPageProps = {
+ articles: EdgesResponse<RawArticle>;
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ totalArticles: number;
+ translation: Messages;
+};
+
+/**
+ * Blog index page.
+ */
+const BlogPage: NextPageWithLayout<BlogPageProps> = ({
+ articles,
+ thematicsList,
+ topicsList,
+ totalArticles,
}) => {
const intl = useIntl();
- const lastPostRef = useRef<HTMLSpanElement>(null);
- const router = useRouter();
- const [isMounted, setIsMounted] = useState<boolean>(false);
-
- useEffect(() => {
- if (typeof window !== undefined) setIsMounted(true);
- }, []);
-
- const getKey = (pageIndex: number, previousData: PostsListData) => {
- if (previousData && !previousData.posts) return null;
-
- return pageIndex === 0
- ? { first: settings.postsPerPage }
- : {
- first: settings.postsPerPage,
- after: previousData.pageInfo.endCursor,
- };
- };
-
- const { data, error, size, setSize } = useSWRInfinite(
- getKey,
- getPublishedPosts,
- { fallbackData: [posts] }
- );
- const [totalPostsCount, setTotalPostsCount] = useState<number>(totalPosts);
-
- useEffect(() => {
- if (data) setTotalPostsCount(data[0].pageInfo.total);
- }, [data]);
-
- const [loadedPostsCount, setLoadedPostsCount] = useState<number>(
- settings.postsPerPage
- );
-
- useEffect(() => {
- if (data && data.length > 0) {
- const newCount =
- settings.postsPerPage +
- data[0].pageInfo.total -
- data[data.length - 1].pageInfo.total;
- setLoadedPostsCount(newCount);
- }
- }, [data]);
-
- const isLoadingInitialData = !data && !error;
- const isLoadingMore: boolean =
- isLoadingInitialData ||
- (size > 0 && data !== undefined && typeof data[size - 1] === 'undefined');
-
- const hasNextPage = data && data[data.length - 1].pageInfo.hasNextPage;
-
- const loadMorePosts = () => {
- if (lastPostRef.current) {
- lastPostRef.current.focus();
- }
- setSize(size + 1);
- };
-
- const getPostsList = () => {
- if (error)
- return intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'BlogPage: failed to load text',
- id: 'C/XGkH',
- });
- if (!data) return <Spinner />;
-
- return <PostsList ref={lastPostRef} data={data} showYears={true} />;
- };
+ const title = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'BlogPage: page title',
+ id: '7TbbIk',
+ });
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: '/blog',
+ });
+ const { blog, website } = useSettings();
+ const { asPath } = useRouter();
const pageTitle = intl.formatMessage(
{
defaultMessage: 'Blog: development, open source - {websiteName}',
description: 'BlogPage: SEO - Page title',
id: '+Y+tLK',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
const pageDescription = intl.formatMessage(
{
@@ -119,44 +82,51 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
description: 'BlogPage: SEO - Meta description',
id: '18h/t0',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
- const pageUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
+ const webpageSchema = getWebPageSchema({
description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: false,
+ locale: website.locales.default,
+ slug: asPath,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, blogSchema]);
+
+ const {
+ data,
+ error,
+ isLoadingInitialData,
+ isLoadingMore,
+ hasNextPage,
+ setSize,
+ } = usePagination<RawArticle>({
+ fallbackData: [articles],
+ fetcher: getArticles,
+ perPage: blog.postsPerPage,
+ });
- const blogSchema: BlogSchema = {
- '@id': `${settings.url}/#blog`,
- '@type': 'Blog',
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
+ /**
+ * Load more posts handler.
+ */
+ const loadMore = () => {
+ setSize((prevSize) => prevSize + 1);
};
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, blogSchema],
- };
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'BlogPage: thematics list widget title',
+ id: 'HriY57',
+ });
- const title = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: page title',
- id: '7TbbIk',
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'BlogPage: topics list widget title',
+ id: '2D9tB5',
});
return (
@@ -164,7 +134,7 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={pageDescription} />
@@ -174,96 +144,82 @@ const Blog: NextPageWithLayout<BlogPageProps> = ({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="blog"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ title={title}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={{ total: totalArticles }}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
>
- <PostHeader title={title} meta={{ results: totalPostsCount }} />
- <div className={styles.body}>
- {getPostsList()}
- {hasNextPage &&
- (isMounted ? (
- <>
- <PaginationCursor
- current={loadedPostsCount}
- total={totalPostsCount}
- />
- <Button
- isDisabled={isLoadingMore}
- clickHandler={loadMorePosts}
- position="center"
- spacing={true}
- >
- {intl.formatMessage({
- defaultMessage: 'Load more?',
- description: 'BlogPage: load more text',
- id: 'Kqq2cm',
- })}
- </Button>
- </>
- ) : (
- <Pagination baseUrl="/blog" total={totalPostsCount} />
- ))}
- </div>
- <Sidebar
- position="right"
- title={intl.formatMessage({
- defaultMessage: 'Filter by:',
- description: 'BlogPage: sidebar title',
- id: 'KERk7L',
- })}
- >
- <ThematicsList
- initialData={allThematics}
- title={intl.formatMessage({
- defaultMessage: 'Thematics',
- description: 'BlogPage: thematics list widget title',
- id: 'HriY57',
- })}
+ {data && (
+ <PostsList
+ baseUrl="/blog/page/"
+ byYear={true}
+ isLoading={isLoadingMore || isLoadingInitialData}
+ loadMore={loadMore}
+ posts={getPostsList(data)}
+ searchPage="/recherche/"
+ showLoadMoreBtn={hasNextPage}
+ total={totalArticles}
/>
- <TopicsList
- initialData={allTopics}
- title={intl.formatMessage({
- defaultMessage: 'Topics',
- description: 'BlogPage: topics list widget title',
- id: '2D9tB5',
+ )}
+ {error && (
+ <Notice
+ kind="error"
+ message={intl.formatMessage({
+ defaultMessage: 'Failed to load.',
+ description: 'BlogPage: failed to load text',
+ id: 'C/XGkH',
})}
/>
- </Sidebar>
- </article>
+ )}
+ </PageLayout>
</>
);
};
-Blog.getLayout = getLayout;
+BlogPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: breadcrumb item',
- id: 'R0eDmw',
- });
- const firstPosts = await getPublishedPosts({ first: settings.postsPerPage });
- const totalPosts = await getPostsTotal();
- const allThematics = await getAllThematics();
- const allTopics = await getAllTopics();
- const { locale } = context;
+export const getStaticProps: GetStaticProps<BlogPageProps> = async ({
+ locale,
+}) => {
+ const articles = await getArticles({ first: settings.postsPerPage });
+ const totalArticles = await getTotalArticles();
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
const translation = await loadTranslation(locale);
return {
props: {
- allThematics,
- allTopics,
- breadcrumbTitle,
- locale,
- posts: firstPosts,
- totalPosts,
+ articles: JSON.parse(JSON.stringify(articles)),
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
+ totalArticles,
translation,
},
};
};
-export default Blog;
+export default BlogPage;
diff --git a/src/pages/blog/page/[id].tsx b/src/pages/blog/page/[id].tsx
deleted file mode 100644
index 6c4d2f8..0000000
--- a/src/pages/blog/page/[id].tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import { getLayout } from '@components/Layouts/Layout';
-import Pagination from '@components/Pagination/Pagination';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostsList from '@components/PostsList/PostsList';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { ThematicsList, TopicsList } from '@components/Widgets';
-import {
- getAllThematics,
- getAllTopics,
- getEndCursor,
- getPostsTotal,
- getPublishedPosts,
-} from '@services/graphql/queries';
-import { NextPageWithLayout } from '@ts/types/app';
-import { BlogPageProps } from '@ts/types/blog';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { useIntl } from 'react-intl';
-import { Blog, Graph, WebPage } from 'schema-dts';
-import styles from '@styles/pages/Page.module.scss';
-import { getFormattedPageNumbers } from '@utils/helpers/format';
-import { useEffect } from 'react';
-import Spinner from '@components/Spinner/Spinner';
-
-const BlogPage: NextPageWithLayout<BlogPageProps> = ({
- allThematics,
- allTopics,
- posts,
- totalPosts,
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const pageNumber = Number(router.query.id);
-
- useEffect(() => {
- if (router.query.id === '1') router.push('/blog');
- }, [router]);
-
- if (router.isFallback) return <Spinner />;
-
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Blog - Page {number} - {websiteName}',
- description: 'BlogPage: SEO - Page title',
- id: '8w+jnD',
- },
- { number: pageNumber, websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage:
- "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.",
- description: 'BlogPage: SEO - Meta description',
- id: '18h/t0',
- },
- { websiteName: settings.name }
- );
- const pageUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
-
- const blogSchema: Blog = {
- '@id': `${settings.url}/#blog`,
- '@type': 'Blog',
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, blogSchema],
- };
-
- const title = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: page title',
- id: '7TbbIk',
- });
-
- return (
- <>
- <Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
- <meta property="og:type" content="website" />
- <meta property="og:title" content={title} />
- <meta property="og:description" content={pageDescription} />
- </Head>
- <Script
- id="schema-blog"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <article
- id="blog"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader title={title} meta={{ results: totalPosts }} />
- <div className={styles.body}>
- <PostsList data={[posts]} showYears={true} />
- <Pagination baseUrl="/blog" total={totalPosts} />
- </div>
- <Sidebar
- position="right"
- title={intl.formatMessage({
- defaultMessage: 'Filter by:',
- description: 'BlogPage: sidebar title',
- id: 'KERk7L',
- })}
- >
- <ThematicsList
- initialData={allThematics}
- title={intl.formatMessage({
- defaultMessage: 'Thematics',
- description: 'BlogPage: thematics list widget title',
- id: 'HriY57',
- })}
- />
- <TopicsList
- initialData={allTopics}
- title={intl.formatMessage({
- defaultMessage: 'Topics',
- description: 'BlogPage: topics list widget title',
- id: '2D9tB5',
- })}
- />
- </Sidebar>
- </article>
- </>
- );
-};
-
-BlogPage.getLayout = getLayout;
-
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Blog',
- description: 'BlogPage: breadcrumb item',
- id: 'R0eDmw',
- });
- const { locale, params } = context;
- const queriedPageNumber = params ? Number(params.id) : 1;
- const queriedPostsNumber = settings.postsPerPage * queriedPageNumber;
- const endCursor =
- queriedPostsNumber === 1
- ? undefined
- : await getEndCursor({ first: queriedPostsNumber });
- const posts = await getPublishedPosts({
- first: settings.postsPerPage,
- after: endCursor,
- });
- const totalPosts = await getPostsTotal();
- const allThematics = await getAllThematics();
- const allTopics = await getAllTopics();
- const translation = await loadTranslation(locale);
-
- return {
- props: {
- allThematics,
- allTopics,
- breadcrumbTitle,
- locale,
- posts,
- totalPosts,
- translation,
- },
- };
-};
-
-export default BlogPage;
-
-export const getStaticPaths: GetStaticPaths = async () => {
- const totalPosts = await getPostsTotal();
- const totalPages = Math.floor(totalPosts / settings.postsPerPage);
- const paths = getFormattedPageNumbers(totalPages);
-
- return {
- paths,
- fallback: true,
- };
-};
diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx
new file mode 100644
index 0000000..1e1240a
--- /dev/null
+++ b/src/pages/blog/page/[number].tsx
@@ -0,0 +1,237 @@
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { type EdgesResponse } from '@services/graphql/api';
+import {
+ getArticles,
+ getArticlesEndCursor,
+ getTotalArticles,
+} from '@services/graphql/articles';
+import {
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { settings } from '@utils/config';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsList,
+} from '@utils/helpers/pages';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useRedirection from '@utils/hooks/use-redirection';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { ParsedUrlQuery } from 'querystring';
+import { useIntl } from 'react-intl';
+
+type BlogPageProps = {
+ articles: EdgesResponse<RawArticle>;
+ pageNumber: number;
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ totalArticles: number;
+ translation: Messages;
+};
+
+/**
+ * Blog index page.
+ */
+const BlogPage: NextPageWithLayout<BlogPageProps> = ({
+ articles,
+ pageNumber,
+ thematicsList,
+ topicsList,
+ totalArticles,
+}) => {
+ useRedirection({
+ query: { param: 'number', value: '1' },
+ redirectTo: '/blog',
+ });
+
+ const intl = useIntl();
+ const title = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'BlogPage: page title',
+ id: '7TbbIk',
+ });
+ const pageNumberTitle = intl.formatMessage(
+ {
+ defaultMessage: 'Page {number}',
+ id: 'zbzlb1',
+ description: 'BlogPage: page number',
+ },
+ {
+ number: pageNumber,
+ }
+ );
+ const pageTitleWithPageNumber = `${title} - ${pageNumberTitle}`;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title: pageNumberTitle,
+ url: `/blog/page/${pageNumber}`,
+ });
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const pageTitle = `${pageTitleWithPageNumber} - ${website.name}`;
+ const pageDescription = intl.formatMessage(
+ {
+ defaultMessage:
+ "Discover {websiteName}'s writings. He talks about web development, Linux and open source mostly.",
+ description: 'BlogPage: SEO - Meta description',
+ id: '18h/t0',
+ },
+ { websiteName: website.name }
+ );
+ const webpageSchema = getWebPageSchema({
+ description: pageDescription,
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: false,
+ locale: website.locales.default,
+ slug: asPath,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, blogSchema]);
+
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'BlogPage: thematics list widget title',
+ id: 'HriY57',
+ });
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'BlogPage: topics list widget title',
+ id: '2D9tB5',
+ });
+
+ return (
+ <>
+ <Head>
+ <title>{pageTitle}</title>
+ <meta name="description" content={pageDescription} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
+ <meta property="og:type" content="website" />
+ <meta property="og:title" content={pageTitleWithPageNumber} />
+ <meta property="og:description" content={pageDescription} />
+ </Head>
+ <Script
+ id="schema-blog"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={pageTitleWithPageNumber}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={{ total: totalArticles }}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
+ >
+ <PostsList
+ baseUrl="/blog/page/"
+ byYear={true}
+ pageNumber={pageNumber}
+ posts={getPostsList([articles])}
+ searchPage="/recherche/"
+ total={totalArticles}
+ />
+ </PageLayout>
+ </>
+ );
+};
+
+BlogPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
+
+interface BlogPageParams extends ParsedUrlQuery {
+ number: string;
+}
+
+export const getStaticProps: GetStaticProps<BlogPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const pageNumber = Number(params!.number as BlogPageParams['number']);
+ const queriedPostsNumber = settings.postsPerPage * pageNumber;
+ const lastCursor = await getArticlesEndCursor({
+ first: queriedPostsNumber,
+ });
+ const articles = await getArticles({
+ first: settings.postsPerPage,
+ after: lastCursor,
+ });
+ const totalArticles = await getTotalArticles();
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
+ const translation = await loadTranslation(locale);
+
+ return {
+ props: {
+ articles: JSON.parse(JSON.stringify(articles)),
+ pageNumber,
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
+ totalArticles,
+ translation,
+ },
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const totalArticles = await getTotalArticles();
+ const totalPages = Math.ceil(totalArticles / settings.postsPerPage);
+ const pagesArray = Array.from(
+ { length: totalPages },
+ (_, index) => index + 1
+ );
+ const paths = pagesArray.map((number) => {
+ return { params: { number: `${number}` } };
+ });
+
+ return {
+ paths,
+ fallback: false,
+ };
+};
+
+export default BlogPage;
diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx
index 5934dd9..2392fe2 100644
--- a/src/pages/contact.tsx
+++ b/src/pages/contact.tsx
@@ -1,89 +1,124 @@
-import ContactForm from '@components/ContactForm/ContactForm';
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { SocialMedia } from '@components/Widgets';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Notice, { type NoticeKind } from '@components/atoms/layout/notice';
+import ContactForm, {
+ type ContactFormProps,
+} from '@components/organisms/forms/contact-form';
+import SocialMedia from '@components/organisms/widgets/social-media';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { meta } from '@content/pages/contact.mdx';
+import { sendMail } from '@services/graphql/contact';
+import styles from '@styles/pages/contact.module.scss';
+import { type NextPageWithLayout } from '@ts/types/app';
+import { loadTranslation } from '@utils/helpers/i18n';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
+import { useState } from 'react';
import { useIntl } from 'react-intl';
-import { ContactPage as ContactPageSchema, Graph, WebPage } from 'schema-dts';
const ContactPage: NextPageWithLayout = () => {
+ const { dates, intro, seo, title } = meta;
const intl = useIntl();
- const router = useRouter();
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/contact`,
+ });
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Contact form - {websiteName}',
- description: 'ContactPage: SEO - Page title',
- id: 'Y3qRib',
- },
- { websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage:
- "Contact {websiteName} through its website. All you need to do it's to fill the contact form.",
- description: 'ContactPage: SEO - Meta description',
- id: 'OIffB4',
- },
- { websiteName: settings.name }
- );
- const pageUrl = `${settings.url}${router.asPath}`;
- const title = intl.formatMessage({
- defaultMessage: 'Contact',
- description: 'ContactPage: page title',
- id: 'AN9iy7',
+ const socialMediaTitle = intl.formatMessage({
+ defaultMessage: 'Find me elsewhere',
+ description: 'ContactPage: social media widget title',
+ id: 'Qh2CwH',
+ });
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
});
- const intro = intl.formatMessage({
- defaultMessage: 'Please fill the form to contact me.',
- description: 'ContactPage: page introduction',
- id: '8Ls2mD',
+ const contactSchema = getSinglePageSchema({
+ dates,
+ description: intro,
+ id: 'contact',
+ kind: 'contact',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
});
+ const schemaJsonLd = getSchemaJson([webpageSchema, contactSchema]);
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ const widgets = [
+ <SocialMedia
+ key="social-media"
+ title={socialMediaTitle}
+ level={2}
+ media={[
+ { name: 'Github', url: 'https://github.com/ArmandPhilippot' },
+ { name: 'Gitlab', url: 'https://gitlab.com/ArmandPhilippot' },
+ {
+ name: 'LinkedIn',
+ url: 'https://www.linkedin.com/in/armandphilippot',
+ },
+ ]}
+ />,
+ ];
- const contactSchema: ContactPageSchema = {
- '@id': `${settings.url}/#contact`,
- '@type': 'ContactPage',
- name: title,
- description: intro,
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
+ const [status, setStatus] = useState<NoticeKind>('info');
+ const [statusMessage, setStatusMessage] = useState<string>('');
+
+ const submitMail: ContactFormProps['sendMail'] = async (data, reset) => {
+ const { email, message, name, subject } = data;
+ const messageHTML = message.replace(/\r?\n/g, '<br />');
+ const body = `Message received from ${name} <${email}> on ${website.url}.<br /><br />${messageHTML}`;
+ const replyTo = `${name} <${email}>`;
+ const mailData = {
+ body,
+ clientMutationId: 'contact',
+ replyTo,
+ subject,
+ };
+ const { message: mutationMessage, sent } = await sendMail(mailData);
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, contactSchema],
+ if (sent) {
+ setStatus('success');
+ setStatusMessage(
+ intl.formatMessage({
+ defaultMessage:
+ 'Thanks. Your message was successfully sent. I will answer it as soon as possible.',
+ description: 'Contact: success message',
+ id: '3Pipok',
+ })
+ );
+ reset();
+ } else {
+ const errorPrefix = intl.formatMessage({
+ defaultMessage: 'An error occurred:',
+ description: 'Contact: error message',
+ id: 'TpyFZ6',
+ });
+ const error = `${errorPrefix} ${mutationMessage}`;
+ setStatus('error');
+ setStatusMessage(error);
+ }
};
return (
<>
<Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={intro} />
@@ -93,56 +128,36 @@ const ContactPage: NextPageWithLayout = () => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="contact"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ intro={intro}
+ title="Contact"
+ widgets={widgets}
>
- <PostHeader title={title} intro={intro} />
- <div className={styles.body}>
- <p>
- {intl.formatMessage({
- defaultMessage: 'All fields marked with * are required.',
- description: 'ContactPage: required fields text',
- id: 'txusHd',
- })}
- </p>
- <ContactForm />
- </div>
- <Sidebar position="right">
- <SocialMedia
- title={intl.formatMessage({
- defaultMessage: 'Find me elsewhere',
- description: 'ContactPage: social media widget title',
- id: 'Qh2CwH',
- })}
- github={true}
- gitlab={true}
- linkedin={true}
- />
- </Sidebar>
- </article>
+ <ContactForm
+ sendMail={submitMail}
+ Notice={
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ }
+ />
+ </PageLayout>
</>
);
};
-ContactPage.getLayout = getLayout;
+ContactPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Contact',
- description: 'ContactPage: breadcrumb item',
- id: 'CzTbM4',
- });
- const { locale } = context;
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
translation,
},
};
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index 71eb449..4686505 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -1,108 +1,164 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { CVPreview, SocialMedia, ToC } from '@components/Widgets';
-import CVContent, { intro, meta, pdf, image } from '@content/pages/cv.mdx';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { settings } from '@utils/config';
+import Heading from '@components/atoms/headings/heading';
+import Link from '@components/atoms/links/link';
+import List from '@components/atoms/lists/list';
+import ImageWidget from '@components/organisms/widgets/image-widget';
+import SocialMedia from '@components/organisms/widgets/social-media';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import CVContent, { data, meta } from '@content/pages/cv.mdx';
+import styles from '@styles/pages/cv.module.scss';
+import { type NextPageWithLayout } from '@ts/types/app';
import { loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { NestedMDXComponents } from 'mdx/types';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
+import React, { ReactNode } from 'react';
import { useIntl } from 'react-intl';
-import { AboutPage, Graph, WebPage } from 'schema-dts';
-const CV: NextPageWithLayout = () => {
+/**
+ * CV page.
+ */
+const CVPage: NextPageWithLayout = () => {
const intl = useIntl();
- const router = useRouter();
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
+ const { file, image } = data;
+ const { dates, intro, seo, title } = meta;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/cv`,
+ });
- const pageMeta: ArticleMeta = {
- dates,
+ const imageWidgetTitle = intl.formatMessage({
+ defaultMessage: 'Others formats',
+ description: 'CVPage: cv preview widget title',
+ id: 'B9OCyV',
+ });
+ const socialMediaTitle = intl.formatMessage({
+ defaultMessage: 'Open-source projects',
+ description: 'CVPage: social media widget title',
+ id: '+Dre5J',
+ });
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: {
+ date: dates.publication,
+ },
+ update: dates.update
+ ? {
+ date: dates.update,
+ }
+ : undefined,
};
- const pageUrl = `${settings.url}${router.asPath}`;
- const pageTitle = intl.formatMessage(
+
+ const { website } = useSettings();
+ const cvAlt = intl.formatMessage(
{
- defaultMessage: 'CV Front-end developer - {websiteName}',
- description: 'CVPage: SEO - Page title',
- id: 'Y1ZdJ6',
+ defaultMessage: '{name} CV',
+ description: 'CVPage: CV image alternative text',
+ id: 'KUowUk',
},
- { websiteName: settings.name }
+ { name: website.name }
);
- const pageDescription = intl.formatMessage(
+ const cvCaption = intl.formatMessage(
{
- defaultMessage:
- 'Discover the curriculum of {websiteName}, front-end developer located in France: skills, experiences and training.',
- description: 'CVPage: SEO - Meta description',
- id: 'bBdMGm',
+ defaultMessage: '<link>Download the CV in PDF</link>',
+ id: 'fN04AJ',
+ description: 'CVPage: download CV in PDF text',
},
- { websiteName: settings.name }
+ {
+ link: (chunks: ReactNode) => (
+ <Link download={true} href={file}>
+ {chunks}
+ </Link>
+ ),
+ }
);
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
+ const widgets = [
+ <ImageWidget
+ key="image-widget"
+ expanded={true}
+ title={imageWidgetTitle}
+ level={2}
+ image={{ alt: cvAlt, ...image }}
+ description={cvCaption}
+ imageClassName={styles.image}
+ />,
+ <SocialMedia
+ key="social-media"
+ title={socialMediaTitle}
+ level={2}
+ media={[
+ { name: 'Github', url: 'https://github.com/ArmandPhilippot' },
+ { name: 'Gitlab', url: 'https://gitlab.com/ArmandPhilippot' },
+ {
+ name: 'LinkedIn',
+ url: 'https://www.linkedin.com/in/armandphilippot',
+ },
+ ]}
+ />,
+ ];
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
-
- const cvSchema: AboutPage = {
- '@id': `${settings.url}/#cv`,
- '@type': 'AboutPage',
- name: pageTitle,
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const cvSchema = getSinglePageSchema({
+ cover: image.src,
+ dates,
description: intro,
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- image,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- thumbnailUrl: image,
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
+ id: 'cv',
+ kind: 'about',
+ locale: website.locales.default,
+ slug: asPath,
+ title: title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, cvSchema]);
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, cvSchema],
+ const components: NestedMDXComponents = {
+ a: (props) => <Link external={true} {...props} />,
+ h1: (props) => <Heading level={1} {...props} />,
+ h2: (props) => <Heading level={2} {...props} />,
+ h3: (props) => <Heading level={3} {...props} />,
+ h4: (props) => <Heading level={4} {...props} />,
+ h5: (props) => <Heading level={5} {...props} />,
+ h6: (props) => <Heading level={6} {...props} />,
+ Link: (props) => <Link {...props} />,
+ List: (props) => <List {...props} />,
};
- const title = intl.formatMessage(
- {
- defaultMessage: "{name}'s CV",
- description: 'CVPage: page title',
- id: 'Mj2BQf',
- },
- { name: settings.name }
- );
-
return (
- <>
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={headerMeta}
+ intro={intro}
+ title={title}
+ widgets={widgets}
+ withToC={true}
+ >
<Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
<meta property="og:title" content={title} />
<meta property="og:description" content={intro} />
- <meta property="og:image" content={image} />
+ <meta property="og:image" content={image.src} />
<meta property="og:image:alt" content={title} />
</Head>
<Script
@@ -110,72 +166,22 @@ const CV: NextPageWithLayout = () => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="cv"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader intro={intro} meta={pageMeta} title={meta.title} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'CVPage: ToC sidebar aria-label',
- id: 'g4DckL',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <CVContent />
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'CVPage: right sidebar aria-label',
- id: 'QHOm5t',
- })}
- >
- <CVPreview
- title={intl.formatMessage({
- defaultMessage: 'Others formats',
- description: 'CVPage: cv preview widget title',
- id: 'B9OCyV',
- })}
- imgSrc={image}
- pdf={pdf}
- />
- <SocialMedia
- title={intl.formatMessage({
- defaultMessage: 'Open-source projects',
- description: 'CVPage: social media widget title',
- id: '+Dre5J',
- })}
- github={true}
- gitlab={true}
- />
- </Sidebar>
- </article>
- </>
+ <CVContent components={components} />
+ </PageLayout>
);
};
-CV.getLayout = getLayout;
+CVPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const breadcrumbTitle = meta.title;
- const { locale } = context;
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
translation,
},
};
};
-export default CV;
+export default CVPage;
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ca0a809..6e9c4c6 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -1,39 +1,59 @@
import FeedIcon from '@assets/images/icon-feed.svg';
-import { ButtonLink } from '@components/Buttons';
-import { ContactIcon } from '@components/Icons';
-import Layout from '@components/Layouts/Layout';
-import { ResponsiveImage } from '@components/MDX';
-import { RecentPosts } from '@components/Widgets';
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Envelop from '@components/atoms/icons/envelop';
+import Column, { type ColumnProps } from '@components/atoms/layout/column';
+import Section, { type SectionProps } from '@components/atoms/layout/section';
+import List, { type ListItem } from '@components/atoms/lists/list';
+import ResponsiveImage, {
+ type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
+import Columns, {
+ type ColumnsProps,
+} from '@components/molecules/layout/columns';
+import CardsList, {
+ type CardsListItem,
+} from '@components/organisms/layout/cards-list';
+import { getLayout } from '@components/templates/layout/layout';
import HomePageContent from '@content/pages/homepage.mdx';
-import { getPublishedPosts } from '@services/graphql/queries';
-import styles from '@styles/pages/Home.module.scss';
-import { NextPageWithLayout, ResponsiveImageProps } from '@ts/types/app';
-import { PostsList } from '@ts/types/blog';
-import { settings } from '@utils/config';
-import { loadTranslation } from '@utils/helpers/i18n';
+import { getArticlesCard } from '@services/graphql/articles';
+import styles from '@styles/pages/home.module.scss';
+import { type ArticleCard, type NextPageWithLayout } from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import { getSchemaJson, getWebPageSchema } from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import Script from 'next/script';
-import type { ReactElement } from 'react';
+import { ReactElement } from 'react';
import { useIntl } from 'react-intl';
-import { Graph, WebPage } from 'schema-dts';
-type HomePageProps = {
- recentPosts: PostsList;
+type HomeProps = {
+ recentPosts: ArticleCard[];
+ translation?: Messages;
};
-const Home: NextPageWithLayout<HomePageProps> = ({
- recentPosts,
-}: {
- recentPosts: PostsList;
-}) => {
+/**
+ * Home page.
+ */
+const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
const intl = useIntl();
+ const { schema: breadcrumbSchema } = useBreadcrumb({
+ title: '',
+ url: `/`,
+ });
- const CodingLinks = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve a list of coding links.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const CodingLinks = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'web-development',
+ value: (
<ButtonLink target="/thematique/developpement-web">
{intl.formatMessage({
defaultMessage: 'Web development',
@@ -41,8 +61,11 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'vkF/RP',
})}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'projects',
+ value: (
<ButtonLink target="/projets">
{intl.formatMessage({
defaultMessage: 'Projects',
@@ -50,38 +73,65 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'N44SOc',
})}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const ColdarkRepos = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve a list of Coldark repositories.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const ColdarkRepos = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'coldark-github',
+ value: (
<ButtonLink
target="https://github.com/ArmandPhilippot/coldark"
- isExternal={true}
+ external={true}
>
- Github
+ {intl.formatMessage({
+ defaultMessage: 'Github',
+ description: 'HomePage: Github link',
+ id: '3f3PzH',
+ })}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'coldark-gitlab',
+ value: (
<ButtonLink
target="https://gitlab.com/ArmandPhilippot/coldark"
- isExternal={true}
+ external={true}
>
- Gitlab
+ {intl.formatMessage({
+ defaultMessage: 'Gitlab',
+ description: 'HomePage: Gitlab link',
+ id: '7AnwZ7',
+ })}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const LibreLinks = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve a list of links related to Free thematic.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const LibreLinks = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'free',
+ value: (
<ButtonLink target="/thematique/libre">
{intl.formatMessage({
defaultMessage: 'Free',
@@ -89,8 +139,11 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'w8GrOf',
})}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'linux',
+ value: (
<ButtonLink target="/thematique/linux">
{intl.formatMessage({
defaultMessage: 'Linux',
@@ -98,15 +151,23 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'jASD7k',
})}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const ShaarliLink = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve the Shaarli link.
+ *
+ * @returns {JSX.Element} - A list of links
+ */
+ const ShaarliLink = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'shaarli',
+ value: (
<ButtonLink target="https://shaarli.armandphilippot.com/">
{intl.formatMessage({
defaultMessage: 'Shaarli',
@@ -114,59 +175,127 @@ const Home: NextPageWithLayout<HomePageProps> = ({
id: 'i5L19t',
})}
</ButtonLink>
- </li>
- </ul>
- );
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
};
- const MoreLinks = () => {
- return (
- <ul className={styles['links-list']}>
- <li>
+ /**
+ * Retrieve the additional links.
+ *
+ * @returns {JSX.Element} - A list of links.
+ */
+ const MoreLinks = (): JSX.Element => {
+ const links: ListItem[] = [
+ {
+ id: 'contact-me',
+ value: (
<ButtonLink target="/contact">
- <ContactIcon />
+ <Envelop className={styles.icon} />
{intl.formatMessage({
defaultMessage: 'Contact me',
description: 'HomePage: contact button text',
id: 'sO/Iwj',
})}
</ButtonLink>
- </li>
- <li>
+ ),
+ },
+ {
+ id: 'rss-feed',
+ value: (
<ButtonLink target="/feed">
- <FeedIcon className={styles['icon--feed']} />
+ <FeedIcon className={`${styles.icon} ${styles['icon--feed']}`} />
{intl.formatMessage({
defaultMessage: 'Subscribe',
description: 'HomePage: RSS feed subscription text',
id: 'T4YA64',
})}
</ButtonLink>
- </li>
- </ul>
+ ),
+ },
+ ];
+
+ return <List kind="flex" items={links} className={styles.list} />;
+ };
+
+ /**
+ * Get a cards list of recent posts.
+ *
+ * @returns {JSX.Element} - The cards list.
+ */
+ const getRecentPosts = (): JSX.Element => {
+ const posts: CardsListItem[] = recentPosts.map((post) => {
+ return {
+ cover: post.cover,
+ id: post.slug,
+ meta: { publication: { date: post.dates.publication } },
+ title: post.title,
+ url: `/article/${post.slug}`,
+ };
+ });
+
+ return (
+ <CardsList
+ items={posts}
+ titleLevel={3}
+ className={`${styles.list} ${styles['list--cards']}`}
+ />
);
};
- const getRecentPosts = () => {
- return <RecentPosts posts={recentPosts} />;
+ /**
+ * Create the page sections.
+ *
+ * @param {object} obj - An object containing the section body.
+ * @param {ReactElement[]} obj.children - The section body.
+ * @returns {JSX.Element} A section element.
+ */
+ const getSection = ({
+ children,
+ variant,
+ }: {
+ children: ReactElement[];
+ variant: SectionProps['variant'];
+ }): JSX.Element => {
+ const [headingEl, ...content] = children;
+ const title = headingEl.props.children;
+
+ return (
+ <Section
+ title={title}
+ content={content}
+ variant={variant}
+ className={styles.section}
+ />
+ );
};
const components: NestedMDXComponents = {
CodingLinks: CodingLinks,
ColdarkRepos: ColdarkRepos,
- Image: (props: ResponsiveImageProps) => ResponsiveImage({ ...props }),
+ Column: (props: ColumnProps) => <Column {...props} />,
+ Columns: (props: ColumnsProps) => (
+ <Columns className={styles.columns} {...props} />
+ ),
+ Image: (props: ResponsiveImageProps) => <ResponsiveImage {...props} />,
LibreLinks: LibreLinks,
MoreLinks: MoreLinks,
RecentPosts: getRecentPosts,
+ Section: getSection,
ShaarliLink: ShaarliLink,
};
+ const { website } = useSettings();
+
const pageTitle = intl.formatMessage(
{
defaultMessage: '{websiteName} | Front-end developer: WordPress/React',
description: 'HomePage: SEO - Page title',
id: 'PXp2hv',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
const pageDescription = intl.formatMessage(
{
@@ -175,35 +304,22 @@ const Home: NextPageWithLayout<HomePageProps> = ({
description: 'HomePage: SEO - Meta description',
id: 'tMuNTy',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
-
- const webpageSchema: WebPage = {
- '@id': `${settings.url}/#home`,
- '@type': 'WebPage',
- name: pageTitle,
+ const webpageSchema = getWebPageSchema({
description: pageDescription,
- author: { '@id': `${settings.url}/#branding` },
- creator: { '@id': `${settings.url}/#branding` },
- editor: { '@id': `${settings.url}/#branding` },
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema],
- };
+ locale: website.locales.default,
+ slug: '',
+ title: pageTitle,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema]);
return (
<>
<Head>
<title>{pageTitle}</title>
<meta name="description" content={pageDescription} />
- <meta property="og:type" content="website" />
- <meta property="og:url" content={`${settings.url}`} />
+ <meta property="og:url" content={website.url} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={pageDescription} />
</Head>
@@ -212,23 +328,22 @@ const Home: NextPageWithLayout<HomePageProps> = ({
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <div id="home">
- <HomePageContent components={components} />
- </div>
+ <Script
+ id="schema-breadcrumb"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+ />
+ <HomePageContent components={components} />
</>
);
};
-Home.getLayout = function getLayout(page: ReactElement) {
- return <Layout isHome={true}>{page}</Layout>;
-};
+HomePage.getLayout = (page) =>
+ getLayout(page, { isHome: true, withExtraPadding: false });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<HomeProps> = async ({ locale }) => {
const translation = await loadTranslation(locale);
- const recentPosts = await getPublishedPosts({ first: 3 });
+ const recentPosts = await getArticlesCard({ first: 3 });
return {
props: {
@@ -238,4 +353,4 @@ export const getStaticProps: GetStaticProps = async (
};
};
-export default Home;
+export default HomePage;
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
index b103b5e..a58a850 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -1,111 +1,86 @@
-import { getLayout } from '@components/Layouts/Layout';
-import { Link } from '@components/MDX';
-import PostHeader from '@components/PostHeader/PostHeader';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { ToC } from '@components/Widgets';
-import LegalNoticeContent, {
- intro,
- meta,
-} from '@content/pages/legal-notice.mdx';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { settings } from '@utils/config';
+import Link from '@components/atoms/links/link';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import LegalNoticeContent, { meta } from '@content/pages/legal-notice.mdx';
+import { type NextPageWithLayout } from '@ts/types/app';
import { loadTranslation } from '@utils/helpers/i18n';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
import { NestedMDXComponents } from 'mdx/types';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
-import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-const LegalNotice: NextPageWithLayout = () => {
- const intl = useIntl();
- const router = useRouter();
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
-
- const pageMeta: ArticleMeta = {
- dates,
- };
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Legal notice - {websiteName}',
- description: 'LegalNoticePage: SEO - Page title',
- id: '4zAUSu',
- },
- { websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage: "Discover the legal notice of {websiteName}'s website.",
- description: 'LegalNoticePage: SEO - Meta description',
- id: 'uvB+32',
- },
- { websiteName: settings.name }
- );
- const pageUrl = `${settings.url}${router.asPath}`;
- const title = intl.formatMessage({
- defaultMessage: 'Legal notice',
- description: 'LegalNoticePage: page title',
- id: '/IirIt',
+/**
+ * Legal Notice page.
+ */
+const LegalNoticePage: NextPageWithLayout = () => {
+ const { dates, intro, seo, title } = meta;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/mentions-legales`,
});
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: {
+ date: dates.publication,
},
- };
-
- const articleSchema: Article = {
- '@id': `${settings.url}/#legal-notice`,
- '@type': 'Article',
- name: title,
- description: intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: title,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
+ update: dates.update
+ ? {
+ date: dates.update,
+ }
+ : undefined,
};
const components: NestedMDXComponents = {
- Link: (props) => Link(props),
+ Image: (props) => <ResponsiveImage {...props} />,
+ Link: (props) => <Link {...props} />,
};
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ dates,
+ description: intro,
+ id: 'legal-notice',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
return (
- <>
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={headerMeta}
+ intro={intro}
+ title={title}
+ withToC={true}
+ >
<Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
- <meta property="og:title" content={pageTitle} />
+ <meta property="og:title" content={`${seo.title} - ${website.name}`} />
<meta property="og:description" content={intro} />
</Head>
<Script
@@ -113,38 +88,22 @@ const LegalNotice: NextPageWithLayout = () => {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="legal-notice"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader intro={intro} meta={pageMeta} title={meta.title} />
- <Sidebar position="left">
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <LegalNoticeContent components={components} />
- </div>
- </article>
- </>
+ <LegalNoticeContent components={components} />
+ </PageLayout>
);
};
-LegalNotice.getLayout = getLayout;
+LegalNoticePage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const breadcrumbTitle = meta.title;
- const { locale } = context;
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
translation,
},
};
};
-export default LegalNotice;
+export default LegalNoticePage;
diff --git a/src/pages/projet/[slug].tsx b/src/pages/projet/[slug].tsx
deleted file mode 100644
index 1f09fed..0000000
--- a/src/pages/projet/[slug].tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { getLayout } from '@components/Layouts/Layout';
-import { CodeBlock, Gallery, Link, ResponsiveImage } from '@components/MDX';
-import PostHeader from '@components/PostHeader/PostHeader';
-import ProjectSummary from '@components/ProjectSummary/ProjectSummary';
-import Sidebar from '@components/Sidebar/Sidebar';
-import { Sharing, ToC } from '@components/Widgets';
-import styles from '@styles/pages/Page.module.scss';
-import {
- NextPageWithLayout,
- Project as ProjectData,
- ProjectProps,
-} from '@ts/types/app';
-import { settings } from '@utils/config';
-import { loadTranslation } from '@utils/helpers/i18n';
-import {
- getAllProjectsFilename,
- getProjectData,
-} from '@utils/helpers/projects';
-import { MDXComponents, NestedMDXComponents } from 'mdx/types';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { ParsedUrlQuery } from 'querystring';
-import { ComponentType } from 'react';
-import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-
-const Project: NextPageWithLayout<ProjectProps> = ({
- project,
-}: {
- project: ProjectData;
-}) => {
- const intl = useIntl();
- const router = useRouter();
- const projectUrl = `${settings.url}${router.asPath}`;
- const { id, intro, meta, title, seo } = project;
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
-
- const components: NestedMDXComponents = {
- CodeBlock: (props) => CodeBlock(props),
- Gallery: (props) => Gallery(props),
- Image: (props) => ResponsiveImage({ caption: props.caption, ...props }),
- Link: (props) => Link(props),
- pre: ({ children }) => CodeBlock(children.props),
- };
-
- const ProjectContent: ComponentType<MDXComponents> =
- require(`../../content/projects/${id}.mdx`).default;
-
- const webpageSchema: WebPage = {
- '@id': `${projectUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: seo.title,
- description: seo.description,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
-
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
-
- const articleSchema: Article = {
- '@id': `${settings.url}/project`,
- '@type': 'Article',
- name: title,
- description: intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: title,
- thumbnailUrl: meta.hasCover ? `/projects/${id}.jpg` : '',
- image: meta.hasCover ? `/projects/${id}.jpg` : '',
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${projectUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
- };
-
- return (
- <>
- <Head>
- <title>{seo.title}</title>
- <meta name="description" content={seo.description} />
- <meta property="og:url" content={`${projectUrl}`} />
- <meta property="og:type" content="article" />
- <meta property="og:title" content={title} />
- <meta property="og:description" content={intro} />
- </Head>
- <Script
- id="schema-project"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <article
- id="project"
- className={`${styles.article} ${styles['article--no-comments']}`}
- >
- <PostHeader title={title} intro={intro} meta={{ dates }} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'ProjectPage: ToC sidebar aria-label',
- id: '6dXfvr',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <ProjectSummary id={id} title={title} meta={meta} />
- <ProjectContent components={components} />
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'ProjectPage: right sidebar aria-label',
- id: 'hHrNd0',
- })}
- >
- <Sharing title={title} excerpt={intro} />
- </Sidebar>
- </article>
- </>
- );
-};
-
-Project.getLayout = getLayout;
-
-interface ProjectParams extends ParsedUrlQuery {
- slug: string;
-}
-
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
- const translation = await loadTranslation(locale);
- const { slug } = context.params as ProjectParams;
- const project = await getProjectData(slug);
- const breadcrumbTitle = project.title;
-
- return {
- props: {
- breadcrumbTitle,
- locale,
- project,
- translation,
- },
- };
-};
-
-export const getStaticPaths: GetStaticPaths = async () => {
- const filenames = getAllProjectsFilename();
- const paths = filenames.map((filename) => {
- return {
- params: {
- slug: filename,
- },
- };
- });
-
- return {
- paths,
- fallback: false,
- };
-};
-
-export default Project;
diff --git a/src/pages/projets.tsx b/src/pages/projets.tsx
deleted file mode 100644
index 8a81f39..0000000
--- a/src/pages/projets.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import ProjectsList from '@components/ProjectsList/ProjectsList';
-import PageContent, { meta } from '@content/pages/projects.mdx';
-import styles from '@styles/pages/Projects.module.scss';
-import { Project } from '@ts/types/app';
-import { settings } from '@utils/config';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { getSortedProjects } from '@utils/helpers/projects';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
-import Head from 'next/head';
-import { useRouter } from 'next/router';
-import Script from 'next/script';
-import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-
-const Projects = ({ projects }: { projects: Project[] }) => {
- const intl = useIntl();
- const dates = {
- publication: meta.publishedOn,
- update: meta.updatedOn,
- };
- const publicationDate = new Date(dates.publication);
- const updateDate = new Date(dates.update);
- const router = useRouter();
- const pageUrl = `${settings.url}${router.asPath}`;
- const pageTitle = intl.formatMessage(
- {
- defaultMessage: 'Projects: open-source makings - {websiteName}',
- description: 'ProjectsPage: SEO - Page title',
- id: 'SX1z3t',
- },
- { websiteName: settings.name }
- );
- const pageDescription = intl.formatMessage(
- {
- defaultMessage:
- 'Discover {websiteName} projects. Mostly related to web development and open source.',
- description: 'ProjectsPage: SEO - Meta description',
- id: 's6U1Xt',
- },
- { websiteName: settings.name }
- );
-
- const webpageSchema: WebPage = {
- '@id': `${pageUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: pageTitle,
- description: pageDescription,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${pageUrl}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
-
- const articleSchema: Article = {
- '@id': `${settings.url}/#projects`,
- '@type': 'Article',
- name: meta.title,
- description: pageDescription,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: meta.title,
- inLanguage: settings.locales.defaultLocale,
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${pageUrl}` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
- };
-
- return (
- <>
- <Head>
- <title>{pageTitle}</title>
- <meta name="description" content={pageDescription} />
- <meta property="og:url" content={`${pageUrl}`} />
- <meta property="og:type" content="article" />
- <meta property="og:title" content={meta.title} />
- <meta property="og:description" content={pageDescription} />
- </Head>
- <Script
- id="schema-projects"
- type="application/ld+json"
- dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
- />
- <article id="projects" className={styles.article}>
- <PostHeader title={meta.title} intro={<PageContent />} />
- <div className={styles.body}>
- {projects.length > 0 && <ProjectsList projects={projects} />}
- </div>
- </article>
- </>
- );
-};
-
-Projects.getLayout = getLayout;
-
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const breadcrumbTitle = meta.title;
- const { locale } = context;
- const projects: Project[] = await getSortedProjects();
- const translation = await loadTranslation(locale);
-
- return {
- props: {
- breadcrumbTitle,
- locale,
- projects,
- translation,
- },
- };
-};
-
-export default Projects;
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
new file mode 100644
index 0000000..247f350
--- /dev/null
+++ b/src/pages/projets/[slug].tsx
@@ -0,0 +1,241 @@
+import Link from '@components/atoms/links/link';
+import SocialLink, {
+ type SocialWebsite,
+} from '@components/atoms/links/social-link';
+import Spinner from '@components/atoms/loaders/spinner';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import Code from '@components/molecules/layout/code';
+import Gallery from '@components/organisms/images/gallery';
+import Overview, {
+ type OverviewMeta,
+} from '@components/organisms/layout/overview';
+import Sharing from '@components/organisms/widgets/sharing';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
+import styles from '@styles/pages/project.module.scss';
+import {
+ type NextPageWithLayout,
+ type ProjectPreview,
+ type Repos,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import { getProjectData, getProjectFilenames } from '@utils/helpers/projects';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import { capitalize } from '@utils/helpers/strings';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useGithubApi, { type RepoData } from '@utils/hooks/use-github-api';
+import useSettings from '@utils/hooks/use-settings';
+import { MDXComponents, NestedMDXComponents } from 'mdx/types';
+import { GetStaticPaths, GetStaticProps } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { ComponentType } from 'react';
+import { useIntl } from 'react-intl';
+
+type ProjectPageProps = {
+ project: ProjectPreview;
+ translation: Messages;
+};
+
+/**
+ * Project page.
+ */
+const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
+ const { id, intro, meta, title } = project;
+ const { cover, dates, license, repos, seo, technologies } = meta;
+ const intl = useIntl();
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/projets/${id}`,
+ });
+
+ const ProjectContent: ComponentType<MDXComponents> =
+ require(`../../content/projects/${id}.mdx`).default;
+
+ const components: NestedMDXComponents = {
+ Code: (props) => <Code {...props} />,
+ Gallery: (props) => <Gallery {...props} />,
+ Image: (props) => <ResponsiveImage withBorders={true} {...props} />,
+ Link: (props) => <Link {...props} />,
+ pre: ({ children }) => <Code {...children.props} />,
+ };
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const pageUrl = `${website.url}${asPath}`;
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: { date: dates.publication },
+ update:
+ dates.update && dates.update !== dates.publication
+ ? { date: dates.update }
+ : undefined,
+ };
+
+ /**
+ * Retrieve the repositories links.
+ *
+ * @param {Repos} repos - A repositories object.
+ * @returns {JSX.Element[]} - An array of SocialLink.
+ */
+ const getReposLinks = (repositories: Repos): JSX.Element[] => {
+ const links = [];
+
+ for (const [name, url] of Object.entries(repositories)) {
+ const socialWebsite = capitalize(name) as SocialWebsite;
+ const socialUrl = `https://${name}.com/${url}`;
+
+ links.push(<SocialLink name={socialWebsite} url={socialUrl} />);
+ }
+
+ return links;
+ };
+
+ const { isError, isLoading, data } = useGithubApi(meta.repos!.github!);
+
+ const getGithubData = (key: keyof RepoData) => {
+ if (isError) return 'Error';
+ if (isLoading || !data) return <Spinner />;
+
+ switch (key) {
+ case 'created_at':
+ return data.created_at;
+ case 'updated_at':
+ return data.updated_at;
+ case 'stargazers_count':
+ const stars = intl.formatMessage(
+ {
+ defaultMessage:
+ '{starsCount, plural, =0 {No stars on Github} one {# star on Github} other {# stars on Github}}',
+ id: 'Gnf1Si',
+ description: 'Projets: Github stars count',
+ },
+ { starsCount: data.stargazers_count }
+ );
+ return (
+ <>
+ ⭐&nbsp;
+ <Link href={`https://github.com/${repos!.github}/stargazers`}>
+ {stars}
+ </Link>
+ </>
+ );
+ }
+ };
+
+ const overviewData: OverviewMeta = {
+ creation: data && { date: getGithubData('created_at') as string },
+ update: data && { date: getGithubData('updated_at') as string },
+ license,
+ popularity: data && getGithubData('stargazers_count'),
+ repositories: repos ? getReposLinks(repos) : undefined,
+ technologies,
+ };
+
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ cover: `/projects/${id}.jpg`,
+ dates,
+ description: intro,
+ id: 'project',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ return (
+ <>
+ <Head>
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${pageUrl}`} />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
+ </Head>
+ <Script
+ id="schema-project"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={title}
+ intro={intro}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={headerMeta}
+ withToC={true}
+ widgets={[
+ <Sharing
+ key="sharing-widget"
+ data={{ excerpt: intro, title, url: pageUrl }}
+ media={[
+ 'diaspora',
+ 'email',
+ 'facebook',
+ 'journal-du-hacker',
+ 'linkedin',
+ 'twitter',
+ ]}
+ className={styles.widget}
+ />,
+ ]}
+ >
+ <Overview cover={cover} meta={overviewData} />
+ <ProjectContent components={components} />
+ </PageLayout>
+ </>
+ );
+};
+
+ProjectPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
+
+export const getStaticProps: GetStaticProps<ProjectPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const translation = await loadTranslation(locale);
+ const { slug } = params!;
+ const project = await getProjectData(slug as string);
+
+ return {
+ props: {
+ project,
+ translation,
+ },
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const filenames = getProjectFilenames();
+ const paths = filenames.map((filename) => {
+ return {
+ params: {
+ slug: filename,
+ },
+ };
+ });
+
+ return {
+ paths,
+ fallback: false,
+ };
+};
+
+export default ProjectPage;
diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx
new file mode 100644
index 0000000..dbca019
--- /dev/null
+++ b/src/pages/projets/index.tsx
@@ -0,0 +1,123 @@
+import Link from '@components/atoms/links/link';
+import CardsList, {
+ type CardsListItem,
+} from '@components/organisms/layout/cards-list';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import PageContent, { meta } from '@content/pages/projects.mdx';
+import styles from '@styles/pages/projects.module.scss';
+import { type NextPageWithLayout, type ProjectCard } from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import { getProjectsCard } from '@utils/helpers/projects';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { NestedMDXComponents } from 'mdx/types';
+import { GetStaticProps } from 'next';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+
+type ProjectsPageProps = {
+ projects: ProjectCard[];
+ translation?: Messages;
+};
+
+/**
+ * Projects page.
+ */
+const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => {
+ const { dates, seo, title } = meta;
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/projets`,
+ });
+
+ const items: CardsListItem[] = projects.map(
+ ({ id, meta: projectMeta, slug, title: projectTitle }) => {
+ const { cover, tagline, technologies } = projectMeta;
+
+ return {
+ cover,
+ id: id as string,
+ meta: { technologies: technologies },
+ tagline,
+ title: projectTitle,
+ url: `/projets/${slug}`,
+ };
+ }
+ );
+
+ const components: NestedMDXComponents = {
+ Links: (props) => <Link {...props} />,
+ };
+
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ dates,
+ description: seo.description,
+ id: 'projects',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ return (
+ <>
+ <Head>
+ <title>{`${seo.title} - ${website.name}`}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content={`${seo.title} - ${website.name}`} />
+ <meta property="og:description" content={seo.description} />
+ </Head>
+ <Script
+ id="schema-projects"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={title}
+ intro={<PageContent components={components} />}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ >
+ <CardsList items={items} titleLevel={2} className={styles.list} />
+ </PageLayout>
+ </>
+ );
+};
+
+ProjectsPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
+
+export const getStaticProps: GetStaticProps<ProjectsPageProps> = async ({
+ locale,
+}) => {
+ const projects = await getProjectsCard();
+ const translation = await loadTranslation(locale);
+
+ return {
+ props: {
+ projects: JSON.parse(JSON.stringify(projects)),
+ translation,
+ },
+ };
+};
+
+export default ProjectsPage;
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index b843f8d..dbbec55 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -1,213 +1,235 @@
-import { Button } from '@components/Buttons';
-import { getLayout } from '@components/Layouts/Layout';
-import PaginationCursor from '@components/PaginationCursor/PaginationCursor';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostsList from '@components/PostsList/PostsList';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { ThematicsList, TopicsList } from '@components/Widgets';
-import { getPublishedPosts } from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { PostsList as PostsListData } from '@ts/types/blog';
-import { settings } from '@utils/config';
-import { getIntlInstance, loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticProps, GetStaticPropsContext } from 'next';
+import Notice from '@components/atoms/layout/notice';
+import Spinner from '@components/atoms/loaders/spinner';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout from '@components/templates/page/page-layout';
+import { getArticles, getTotalArticles } from '@services/graphql/articles';
+import {
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import { getTopicsPreview, getTotalTopics } from '@services/graphql/topics';
+import { type NextPageWithLayout } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsList,
+} from '@utils/helpers/pages';
+import {
+ getBlogSchema,
+ getSchemaJson,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useDataFromAPI from '@utils/hooks/use-data-from-api';
+import usePagination from '@utils/hooks/use-pagination';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
-import { useEffect, useRef, useState } from 'react';
+import Script from 'next/script';
import { useIntl } from 'react-intl';
-import useSWRInfinite from 'swr/infinite';
-const Search: NextPageWithLayout = () => {
- const intl = useIntl();
- const [query, setQuery] = useState('');
- const router = useRouter();
- const lastPostRef = useRef<HTMLSpanElement>(null);
-
- useEffect(() => {
- if (!router.isReady) return;
-
- if (router.query?.s && typeof router.query.s === 'string') {
- setQuery(router.query.s);
- }
- }, [router.isReady, router.query.s]);
-
- const getKey = (pageIndex: number, previousData: PostsListData) => {
- if (previousData && !previousData.posts) return null;
-
- return pageIndex === 0
- ? { first: settings.postsPerPage, searchQuery: query }
- : {
- first: settings.postsPerPage,
- after: previousData.pageInfo.endCursor,
- searchQuery: query,
- };
- };
-
- const { data, error, size, setSize } = useSWRInfinite(
- getKey,
- getPublishedPosts
- );
- const [totalPostsCount, setTotalPostsCount] = useState<number>(0);
-
- useEffect(() => {
- if (data) setTotalPostsCount(data[0].pageInfo.total);
- }, [data]);
-
- const [loadedPostsCount, setLoadedPostsCount] = useState<number>(
- settings.postsPerPage
- );
-
- useEffect(() => {
- if (data && data.length > 0) {
- const newCount =
- settings.postsPerPage +
- data[0].pageInfo.total -
- data[data.length - 1].pageInfo.total;
- setLoadedPostsCount(newCount);
- }
- }, [data]);
-
- const isLoadingInitialData = !data && !error;
- const isLoadingMore: boolean =
- isLoadingInitialData ||
- (size > 0 && data !== undefined && typeof data[size - 1] === 'undefined');
-
- const hasNextPage = data && data[data.length - 1].pageInfo.hasNextPage;
+type SearchPageProps = {
+ thematicsList: RawThematicPreview[];
+ topicsList: RawTopicPreview[];
+ translation: Messages;
+};
- const title = query
+/**
+ * Search page.
+ */
+const SearchPage: NextPageWithLayout<SearchPageProps> = ({
+ thematicsList,
+ topicsList,
+}) => {
+ const intl = useIntl();
+ const { asPath, query } = useRouter();
+ const title = query.s
? intl.formatMessage(
{
defaultMessage: 'Search results for {query}',
- description: 'SearchPage: search results text',
- id: 'VSGuGE',
+ description: 'SearchPage: SEO - Page title',
+ id: 'ZNBhDP',
},
- { query }
+ { query: query.s as string }
)
: intl.formatMessage({
defaultMessage: 'Search',
- description: 'SearchPage: page title',
- id: 'U+35YD',
+ description: 'SearchPage: SEO - Page title',
+ id: 'WDwNDl',
});
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/recherche`,
+ });
- const description = query
+ const { blog, website } = useSettings();
+ const pageTitle = `${title} - ${website.name}`;
+ const pageDescription = query.s
? intl.formatMessage(
{
- defaultMessage: 'Discover search results for {query}',
- description: 'SearchPage: meta description with query',
- id: 'A4LTGq',
+ defaultMessage:
+ 'Discover search results for {query} on {websiteName}.',
+ description: 'SearchPage: SEO - Meta description',
+ id: 'pg26sn',
},
- { query }
+ { query: query.s as string, websiteName: website.name }
)
: intl.formatMessage(
{
- defaultMessage: 'Search for a post on {websiteName}',
- description: 'SearchPage: meta description without query',
- id: 'PrIz5o',
+ defaultMessage: 'Search for a post on {websiteName}.',
+ description: 'SearchPage: SEO - Meta description',
+ id: 'npisb3',
},
- { websiteName: settings.name }
+ { websiteName: website.name }
);
+ const webpageSchema = getWebPageSchema({
+ description: pageDescription,
+ locale: website.locales.default,
+ slug: asPath,
+ title: pageTitle,
+ });
+ const blogSchema = getBlogSchema({
+ isSinglePage: false,
+ locale: website.locales.default,
+ slug: asPath,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, blogSchema]);
+
+ const {
+ data,
+ error,
+ isLoadingInitialData,
+ isLoadingMore,
+ hasNextPage,
+ setSize,
+ } = usePagination<RawArticle>({
+ fallbackData: [],
+ fetcher: getArticles,
+ perPage: blog.postsPerPage,
+ search: query.s as string,
+ });
- const head = {
- title: `${title} | ${settings.name}`,
- description,
- };
+ const totalArticles = useDataFromAPI<number>(() =>
+ getTotalArticles(query.s as string)
+ );
- const loadMorePosts = () => {
- if (lastPostRef.current) {
- lastPostRef.current.focus();
- }
- setSize(size + 1);
+ /**
+ * Load more posts handler.
+ */
+ const loadMore = () => {
+ setSize((prevSize) => prevSize + 1);
};
- const getPostsList = () => {
- if (error)
- return intl.formatMessage({
- defaultMessage: 'Failed to load.',
- description: 'SearchPage: failed to load text',
- id: 'fOe8rH',
- });
- if (!data) return <Spinner />;
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Thematics',
+ description: 'SearchPage: thematics list widget title',
+ id: 'Dq6+WH',
+ });
- return <PostsList ref={lastPostRef} data={data} showYears={false} />;
- };
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Topics',
+ description: 'SearchPage: topics list widget title',
+ id: 'N804XO',
+ });
return (
<>
<Head>
- <title>{head.title}</title>
- <meta name="description" content={head.description} />
+ <title>{pageTitle}</title>
+ <meta name="description" content={pageDescription} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
+ <meta property="og:type" content="website" />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={pageDescription} />
</Head>
- <article
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <Script
+ id="schema-blog"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+ />
+ <PageLayout
+ title={title}
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ headerMeta={{ total: totalArticles }}
+ widgets={[
+ <LinksListWidget
+ key="thematics-list"
+ items={getLinksListItems(
+ thematicsList.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ )
+ )}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics-list"
+ items={getLinksListItems(
+ topicsList.map((topic) => getPageLinkFromRawData(topic, 'topic'))
+ )}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]}
>
- <PostHeader title={title} meta={{ results: totalPostsCount }} />
- <div className={styles.body}>
- {getPostsList()}
- {hasNextPage && (
- <>
- <PaginationCursor
- current={loadedPostsCount}
- total={totalPostsCount}
- />
- <Button
- isDisabled={isLoadingMore}
- clickHandler={loadMorePosts}
- position="center"
- spacing={true}
- >
- {intl.formatMessage({
- defaultMessage: 'Load more?',
- description: 'SearchPage: load more text',
- id: 'pEtJik',
- })}
- </Button>
- </>
- )}
- </div>
- <Sidebar position="right">
- <ThematicsList
- title={intl.formatMessage({
- defaultMessage: 'Thematics',
- description: 'SearchPage: thematics list widget title',
- id: 'Dq6+WH',
- })}
+ {data && data.length > 0 ? (
+ <PostsList
+ baseUrl="/recherche/page/"
+ byYear={true}
+ isLoading={isLoadingMore || isLoadingInitialData}
+ loadMore={loadMore}
+ posts={getPostsList(data)}
+ searchPage="/recherche/"
+ showLoadMoreBtn={hasNextPage}
+ total={totalArticles || 0}
/>
- <TopicsList
- title={intl.formatMessage({
- defaultMessage: 'Topics',
- description: 'SearchPage: topics list widget title',
- id: 'N804XO',
+ ) : (
+ <Spinner />
+ )}
+ {error && (
+ <Notice
+ kind="error"
+ message={intl.formatMessage({
+ defaultMessage: 'Failed to load.',
+ description: 'SearchPage: failed to load text',
+ id: 'fOe8rH',
})}
/>
- </Sidebar>
- </article>
+ )}
+ </PageLayout>
</>
);
};
-Search.getLayout = getLayout;
+SearchPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const intl = await getIntlInstance();
- const breadcrumbTitle = intl.formatMessage({
- defaultMessage: 'Search',
- description: 'SearchPage: breadcrumb item',
- id: 'TfU6Qm',
- });
- const { locale } = context;
+export const getStaticProps: GetStaticProps<SearchPageProps> = async ({
+ locale,
+}) => {
+ const totalThematics = await getTotalThematics();
+ const thematics = await getThematicsPreview({ first: totalThematics });
+ const totalTopics = await getTotalTopics();
+ const topics = await getTopicsPreview({ first: totalTopics });
const translation = await loadTranslation(locale);
return {
props: {
- breadcrumbTitle,
- locale,
+ thematicsList: thematics.edges.map((edge) => edge.node),
+ topicsList: topics.edges.map((edge) => edge.node),
translation,
},
};
};
-export default Search;
+export default SearchPage;
diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx
index 30dd36c..48924e5 100644
--- a/src/pages/sujet/[slug].tsx
+++ b/src/pages/sujet/[slug].tsx
@@ -1,224 +1,230 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostPreview from '@components/PostPreview/PostPreview';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { RelatedThematics, ToC, TopicsList } from '@components/Widgets';
+import Heading from '@components/atoms/headings/heading';
+import ResponsiveImage from '@components/molecules/images/responsive-image';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
import {
- getAllTopics,
- getAllTopicsSlug,
+ getAllTopicsSlugs,
getTopicBySlug,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { TopicProps, ThematicPreview } from '@ts/types/taxonomies';
-import { settings } from '@utils/config';
-import { getFormattedPaths } from '@utils/helpers/format';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+ getTopicsPreview,
+ getTotalTopics,
+} from '@services/graphql/topics';
+import styles from '@styles/pages/topic.module.scss';
+import {
+ type NextPageWithLayout,
+ type PageLink,
+ type Topic,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsWithUrl,
+} from '@utils/helpers/pages';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
import { ParsedUrlQuery } from 'querystring';
-import { useRef } from 'react';
import { useIntl } from 'react-intl';
-import { Article as Article, Graph, WebPage } from 'schema-dts';
-
-const Topic: NextPageWithLayout<TopicProps> = ({ topic, allTopics }) => {
- const intl = useIntl();
- const relatedThematics = useRef<ThematicPreview[]>([]);
- const router = useRouter();
-
- if (router.isFallback) return <Spinner />;
-
- const updateRelatedThematics = (newThematics: ThematicPreview[]) => {
- newThematics.forEach((thematic) => {
- const thematicIndex = relatedThematics.current.findIndex(
- (relatedThematic) => relatedThematic.id === thematic.id
- );
- const hasThematic = thematicIndex === -1 ? false : true;
-
- if (!hasThematic) relatedThematics.current.push(thematic);
- });
- };
-
- const getPostsList = () => {
- return [...topic.posts].reverse().map((post) => {
- updateRelatedThematics(post.thematics);
-
- return (
- <li key={post.id} className={styles.item}>
- <PostPreview post={post} titleLevel={3} />
- </li>
- );
- });
- };
-
- const meta: ArticleMeta = {
- dates: topic.dates,
- results: topic.posts.length,
- website: topic.officialWebsite,
- };
- const topicUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${topicUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: topic.seo.title,
- description: topic.seo.metaDesc,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- isPartOf: {
- '@id': `${settings.url}`,
- },
- };
- const publicationDate = new Date(topic.dates.publication);
- const updateDate = new Date(topic.dates.update);
+export type TopicPageProps = {
+ currentTopic: Topic;
+ topics: PageLink[];
+ translation: Messages;
+};
- const articleSchema: Article = {
- '@id': `${settings.url}/#topic`,
- '@type': 'Article',
- name: topic.title,
- description: topic.intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: topic.title,
- thumbnailUrl: topic.featuredImage?.sourceUrl,
- image: topic.featuredImage?.sourceUrl,
- inLanguage: settings.locales.defaultLocale,
- isPartOf: { '@id': `${settings.url}/blog` },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${topicUrl}` },
- subjectOf: { '@id': `${settings.url}/blog` },
+const TopicPage: NextPageWithLayout<TopicPageProps> = ({
+ currentTopic,
+ topics,
+}) => {
+ const { content, intro, meta, slug, title } = currentTopic;
+ const {
+ articles,
+ cover,
+ dates,
+ seo,
+ thematics,
+ website: officialWebsite,
+ } = meta;
+ const intl = useIntl();
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/sujet/${slug}`,
+ });
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: { date: dates.publication },
+ update: dates.update ? { date: dates.update } : undefined,
+ website: officialWebsite,
+ total: articles ? articles.length : undefined,
};
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ cover: cover?.src,
+ dates,
+ description: intro,
+ id: 'topic',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Other topics',
+ description: 'TopicPage: other topics list widget title',
+ id: 'JpC3JH',
+ });
+
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Related thematics',
+ description: 'TopicPage: related thematics list widget title',
+ id: '/sRqPT',
+ });
+
+ const getPageHeading = () => {
+ return (
+ <>
+ {cover && <ResponsiveImage className={styles.logo} {...cover} />}
+ {title}
+ </>
+ );
};
return (
<>
<Head>
- <title>{topic.seo.title}</title>
- <meta name="description" content={topic.seo.metaDesc} />
- <meta property="og:url" content={`${topicUrl}`} />
+ <title>{seo.title}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
- <meta property="og:title" content={topic.title} />
- <meta property="og:description" content={topic.intro} />
- <meta property="og:image" content={topic.featuredImage?.sourceUrl} />
- <meta property="og:image:alt" content={topic.featuredImage?.altText} />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
</Head>
<Script
- id="schema-subject"
+ id="schema-project"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="topic"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ title={getPageHeading()}
+ intro={intro}
+ headerMeta={headerMeta}
+ widgets={
+ thematics
+ ? [
+ <LinksListWidget
+ key="related-thematics"
+ items={getLinksListItems(thematics)}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="topics"
+ items={getLinksListItems(topics)}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]
+ : []
+ }
>
- <PostHeader
- cover={topic.featuredImage}
- intro={topic.intro}
- meta={meta}
- title={topic.title}
- />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'TopicPage: ToC sidebar aria-label',
- id: 'lsDB5G',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <div dangerouslySetInnerHTML={{ __html: topic.content }}></div>
- {topic.posts.length > 0 && (
- <section className={styles.section}>
- <h2>
- {intl.formatMessage(
- {
- defaultMessage: 'All posts in {name}',
- description: 'TopicPage: posts list title',
- id: 'FLkF2R',
- },
- { name: topic.title }
- )}
- </h2>
- <ol className={styles.list}>{getPostsList()}</ol>
- </section>
- )}
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'TopicPage: right sidebar aria-label',
- id: 'eu3beS',
- })}
- >
- <RelatedThematics thematics={relatedThematics.current} />
- <TopicsList
- initialData={allTopics}
- title={intl.formatMessage({
- defaultMessage: 'Others topics',
- description: 'TopicPage: topics list widget title',
- id: '+4tiVb',
- })}
- />
- </Sidebar>
- </article>
+ {content && <div dangerouslySetInnerHTML={{ __html: content }} />}
+ {articles && (
+ <>
+ <Heading level={2}>
+ {intl.formatMessage(
+ {
+ defaultMessage: 'All posts in {topicName}',
+ description: 'TopicPage: posts list heading',
+ id: 'zEN3fd',
+ },
+ { topicName: title }
+ )}
+ </Heading>
+ <PostsList
+ baseUrl="/sujet/page/"
+ byYear={true}
+ posts={getPostsWithUrl(articles)}
+ searchPage="/recherche/"
+ titleLevel={3}
+ total={articles.length}
+ />
+ </>
+ )}
+ </PageLayout>
</>
);
};
-Topic.getLayout = getLayout;
+TopicPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-interface PostParams extends ParsedUrlQuery {
+interface TopicParams extends ParsedUrlQuery {
slug: string;
}
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<TopicPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const currentTopic = await getTopicBySlug(
+ params!.slug as TopicParams['slug']
+ );
+ const totalTopics = await getTotalTopics();
+ const allTopicsEdges = await getTopicsPreview({
+ first: totalTopics,
+ });
+ const allTopics = allTopicsEdges.edges.map((edge) =>
+ getPageLinkFromRawData(edge.node, 'topic')
+ );
+ const topicsLinks = allTopics.filter(
+ (topic) => topic.url !== `/sujet/${params!.slug as TopicParams['slug']}`
+ );
const translation = await loadTranslation(locale);
- const { slug } = context.params as PostParams;
- const topic = await getTopicBySlug(slug);
- const allTopics = await getAllTopics();
- const breadcrumbTitle = topic.title;
return {
props: {
- allTopics,
- breadcrumbTitle,
- locale,
- topic,
+ currentTopic: JSON.parse(JSON.stringify(currentTopic)),
+ topics: JSON.parse(JSON.stringify(topicsLinks)),
translation,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
- const allTopics = await getAllTopicsSlug();
- const paths = getFormattedPaths(allTopics);
+ const slugs = await getAllTopicsSlugs();
+ const paths = slugs.map((slug) => {
+ return { params: { slug } };
+ });
return {
paths,
- fallback: true,
+ fallback: false,
};
};
-export default Topic;
+export default TopicPage;
diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx
index db22214..7aa6c1c 100644
--- a/src/pages/thematique/[slug].tsx
+++ b/src/pages/thematique/[slug].tsx
@@ -1,214 +1,211 @@
-import { getLayout } from '@components/Layouts/Layout';
-import PostHeader from '@components/PostHeader/PostHeader';
-import PostPreview from '@components/PostPreview/PostPreview';
-import Sidebar from '@components/Sidebar/Sidebar';
-import Spinner from '@components/Spinner/Spinner';
-import { RelatedTopics, ThematicsList, ToC } from '@components/Widgets';
+import Heading from '@components/atoms/headings/heading';
+import PostsList from '@components/organisms/layout/posts-list';
+import LinksListWidget from '@components/organisms/widgets/links-list-widget';
+import { getLayout } from '@components/templates/layout/layout';
+import PageLayout, {
+ type PageLayoutProps,
+} from '@components/templates/page/page-layout';
import {
- getAllThematics,
- getAllThematicsSlug,
+ getAllThematicsSlugs,
getThematicBySlug,
-} from '@services/graphql/queries';
-import styles from '@styles/pages/Page.module.scss';
-import { NextPageWithLayout } from '@ts/types/app';
-import { ArticleMeta } from '@ts/types/articles';
-import { TopicPreview, ThematicProps } from '@ts/types/taxonomies';
-import { settings } from '@utils/config';
-import { getFormattedPaths } from '@utils/helpers/format';
-import { loadTranslation } from '@utils/helpers/i18n';
-import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from 'next';
+ getThematicsPreview,
+ getTotalThematics,
+} from '@services/graphql/thematics';
+import {
+ type NextPageWithLayout,
+ type PageLink,
+ type Thematic,
+} from '@ts/types/app';
+import { loadTranslation, type Messages } from '@utils/helpers/i18n';
+import {
+ getLinksListItems,
+ getPageLinkFromRawData,
+ getPostsWithUrl,
+} from '@utils/helpers/pages';
+import {
+ getSchemaJson,
+ getSinglePageSchema,
+ getWebPageSchema,
+} from '@utils/helpers/schema-org';
+import useBreadcrumb from '@utils/hooks/use-breadcrumb';
+import useSettings from '@utils/hooks/use-settings';
+import { GetStaticPaths, GetStaticProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
import { ParsedUrlQuery } from 'querystring';
-import { useRef } from 'react';
import { useIntl } from 'react-intl';
-import { Article, Graph, WebPage } from 'schema-dts';
-const Thematic: NextPageWithLayout<ThematicProps> = ({
- thematic,
- allThematics,
+export type ThematicPageProps = {
+ currentThematic: Thematic;
+ thematics: PageLink[];
+ translation: Messages;
+};
+
+const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({
+ currentThematic,
+ thematics,
}) => {
+ const { content, intro, meta, slug, title } = currentThematic;
+ const { articles, dates, seo, topics } = meta;
const intl = useIntl();
- const relatedTopics = useRef<TopicPreview[]>([]);
- const router = useRouter();
-
- if (router.isFallback) return <Spinner />;
-
- const updateRelatedTopics = (newTopics: TopicPreview[]) => {
- newTopics.forEach((topic) => {
- const topicIndex = relatedTopics.current.findIndex(
- (relatedTopic) => relatedTopic.id === topic.id
- );
- const hasTopic = topicIndex === -1 ? false : true;
-
- if (!hasTopic) relatedTopics.current.push(topic);
- });
- };
-
- const getPostsList = () => {
- return [...thematic.posts].reverse().map((post) => {
- updateRelatedTopics(post.topics);
-
- return (
- <li key={post.id} className={styles.item}>
- <PostPreview post={post} titleLevel={3} />
- </li>
- );
- });
+ const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({
+ title,
+ url: `/thematique/${slug}`,
+ });
+
+ const headerMeta: PageLayoutProps['headerMeta'] = {
+ publication: { date: dates.publication },
+ update: dates.update ? { date: dates.update } : undefined,
+ total: articles ? articles.length : undefined,
};
- const meta: ArticleMeta = {
- dates: thematic.dates,
- results: thematic.posts.length,
- };
- const thematicUrl = `${settings.url}${router.asPath}`;
-
- const webpageSchema: WebPage = {
- '@id': `${thematicUrl}`,
- '@type': 'WebPage',
- breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
- name: thematic.seo.title,
- description: thematic.seo.metaDesc,
- inLanguage: settings.locales.defaultLocale,
- reviewedBy: { '@id': `${settings.url}/#branding` },
- url: `${settings.url}`,
- };
-
- const publicationDate = new Date(thematic.dates.publication);
- const updateDate = new Date(thematic.dates.update);
-
- const articleSchema: Article = {
- '@id': `${settings.url}/#thematic`,
- '@type': 'Article',
- name: thematic.title,
- description: thematic.intro,
- author: { '@id': `${settings.url}/#branding` },
- copyrightYear: publicationDate.getFullYear(),
- creator: { '@id': `${settings.url}/#branding` },
- dateCreated: publicationDate.toISOString(),
- dateModified: updateDate.toISOString(),
- datePublished: publicationDate.toISOString(),
- editor: { '@id': `${settings.url}/#branding` },
- headline: thematic.title,
- inLanguage: settings.locales.defaultLocale,
- isPartOf: { '@id': `${settings.url}/blog` },
- license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
- mainEntityOfPage: { '@id': `${thematicUrl}` },
- subjectOf: { '@id': `${settings.url}/blog` },
- };
-
- const schemaJsonLd: Graph = {
- '@context': 'https://schema.org',
- '@graph': [webpageSchema, articleSchema],
- };
+ const { website } = useSettings();
+ const { asPath } = useRouter();
+ const webpageSchema = getWebPageSchema({
+ description: seo.description,
+ locale: website.locales.default,
+ slug: asPath,
+ title: seo.title,
+ updateDate: dates.update,
+ });
+ const articleSchema = getSinglePageSchema({
+ dates,
+ description: intro,
+ id: 'thematic',
+ kind: 'page',
+ locale: website.locales.default,
+ slug: asPath,
+ title,
+ });
+ const schemaJsonLd = getSchemaJson([webpageSchema, articleSchema]);
+
+ const thematicsListTitle = intl.formatMessage({
+ defaultMessage: 'Other thematics',
+ description: 'ThematicPage: other thematics list widget title',
+ id: 'KVSWGP',
+ });
+
+ const topicsListTitle = intl.formatMessage({
+ defaultMessage: 'Related topics',
+ description: 'ThematicPage: related topics list widget title',
+ id: '/42Z0z',
+ });
return (
<>
<Head>
- <title>{thematic.seo.title}</title>
- <meta name="description" content={thematic.seo.metaDesc} />
- <meta property="og:url" content={`${thematic}`} />
+ <title>{seo.title}</title>
+ <meta name="description" content={seo.description} />
+ <meta property="og:url" content={`${website.url}${asPath}`} />
<meta property="og:type" content="article" />
- <meta property="og:title" content={thematic.title} />
- <meta property="og:description" content={thematic.intro} />
+ <meta property="og:title" content={title} />
+ <meta property="og:description" content={intro} />
</Head>
<Script
- id="schema-thematic"
+ id="schema-project"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
/>
- <article
- id="thematic"
- className={`${styles.article} ${styles['article--no-comments']}`}
+ <PageLayout
+ breadcrumb={breadcrumbItems}
+ breadcrumbSchema={breadcrumbSchema}
+ title={title}
+ intro={intro}
+ headerMeta={headerMeta}
+ widgets={
+ topics
+ ? [
+ <LinksListWidget
+ key="thematics"
+ items={getLinksListItems(thematics)}
+ title={thematicsListTitle}
+ level={2}
+ />,
+ <LinksListWidget
+ key="related-topics"
+ items={getLinksListItems(topics)}
+ title={topicsListTitle}
+ level={2}
+ />,
+ ]
+ : []
+ }
>
- <PostHeader intro={thematic.intro} meta={meta} title={thematic.title} />
- <Sidebar
- position="left"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Table of Contents',
- description: 'ThematicPage: ToC sidebar aria-label',
- id: 'YwvYfw',
- })}
- >
- <ToC />
- </Sidebar>
- <div className={styles.body}>
- <div dangerouslySetInnerHTML={{ __html: thematic.content }}></div>
- {thematic.posts.length > 0 && (
- <section className={styles.section}>
- <h2>
- {intl.formatMessage(
- {
- defaultMessage: 'All posts in {name}',
- description: 'ThematicPage: posts list title',
- id: 'P7fxX2',
- },
- { name: thematic.title }
- )}
- </h2>
- <ol className={styles.list}>{getPostsList()}</ol>
- </section>
- )}
- </div>
- <Sidebar
- position="right"
- ariaLabel={intl.formatMessage({
- defaultMessage: 'Sidebar',
- description: 'ThematicPage: right sidebar aria-label',
- id: 'syLgY9',
- })}
- >
- <RelatedTopics topics={relatedTopics.current} />
- <ThematicsList
- initialData={allThematics}
- title={intl.formatMessage({
- defaultMessage: 'Others thematics',
- description: 'ThematicPage: thematics list widget title',
- id: 'norrGp',
- })}
- />
- </Sidebar>
- </article>
+ <div dangerouslySetInnerHTML={{ __html: content }} />
+ {articles && (
+ <>
+ <Heading level={2}>
+ {intl.formatMessage(
+ {
+ defaultMessage: 'All posts in {thematicName}',
+ description: 'ThematicPage: posts list heading',
+ id: 'LszkU6',
+ },
+ { thematicName: title }
+ )}
+ </Heading>
+ <PostsList
+ baseUrl="/thematique/page/"
+ byYear={true}
+ posts={getPostsWithUrl(articles)}
+ searchPage="/recherche/"
+ titleLevel={3}
+ total={articles.length}
+ />
+ </>
+ )}
+ </PageLayout>
</>
);
};
-Thematic.getLayout = getLayout;
+ThematicPage.getLayout = (page) =>
+ getLayout(page, { useGrid: true, withExtraPadding: true });
-interface PostParams extends ParsedUrlQuery {
+interface ThematicParams extends ParsedUrlQuery {
slug: string;
}
-export const getStaticProps: GetStaticProps = async (
- context: GetStaticPropsContext
-) => {
- const { locale } = context;
+export const getStaticProps: GetStaticProps<ThematicPageProps> = async ({
+ locale,
+ params,
+}) => {
+ const currentThematic = await getThematicBySlug(
+ params!.slug as ThematicParams['slug']
+ );
+ const totalThematics = await getTotalThematics();
+ const allThematicsEdges = await getThematicsPreview({
+ first: totalThematics,
+ });
+ const allThematics = allThematicsEdges.edges.map((edge) =>
+ getPageLinkFromRawData(edge.node, 'thematic')
+ );
+ const allThematicsLinks = allThematics.filter(
+ (thematic) =>
+ thematic.url !== `/thematique/${params!.slug as ThematicParams['slug']}`
+ );
const translation = await loadTranslation(locale);
- const { slug } = context.params as PostParams;
- const thematic = await getThematicBySlug(slug);
- const allThematics = await getAllThematics();
- const breadcrumbTitle = thematic.title;
return {
props: {
- allThematics,
- breadcrumbTitle,
- locale,
- thematic,
+ currentThematic: JSON.parse(JSON.stringify(currentThematic)),
+ thematics: JSON.parse(JSON.stringify(allThematicsLinks)),
translation,
},
};
};
export const getStaticPaths: GetStaticPaths = async () => {
- const allSlugs = await getAllThematicsSlug();
- const paths = getFormattedPaths(allSlugs);
+ const slugs = await getAllThematicsSlugs();
+ const paths = slugs.map((slug) => {
+ return { params: { slug } };
+ });
return {
paths,
- fallback: true,
+ fallback: false,
};
};
-export default Thematic;
+export default ThematicPage;
diff --git a/src/services/graphql/api.ts b/src/services/graphql/api.ts
index a5be026..009aea4 100644
--- a/src/services/graphql/api.ts
+++ b/src/services/graphql/api.ts
@@ -1,25 +1,312 @@
-import { RequestType, VariablesType } from '@ts/types/app';
import { settings } from '@utils/config';
-import { GraphQLClient } from 'graphql-request';
+import {
+ articleBySlugQuery,
+ articlesCardQuery,
+ articlesEndCursor,
+ articlesQuery,
+ articlesSlugQuery,
+ totalArticlesQuery,
+} from './articles.query';
+import { sendCommentMutation } from './comments.mutation';
+import { commentsQuery } from './comments.query';
+import { sendMailMutation } from './contact.mutation';
+import {
+ thematicBySlugQuery,
+ thematicsListQuery,
+ thematicsSlugQuery,
+ totalThematicsQuery,
+} from './thematics.query';
+import {
+ topicBySlugQuery,
+ topicsListQuery,
+ topicsSlugQuery,
+ totalTopicsQuery,
+} from './topics.query';
-export const getGraphQLClient = (): GraphQLClient => {
- const apiUrl = settings.api.url;
+export type Mutations = typeof sendMailMutation | typeof sendCommentMutation;
- if (!apiUrl) throw new Error('API URL not defined.');
+export type Queries =
+ | typeof articlesQuery
+ | typeof articleBySlugQuery
+ | typeof articlesCardQuery
+ | typeof articlesEndCursor
+ | typeof articlesSlugQuery
+ | typeof commentsQuery
+ | typeof thematicBySlugQuery
+ | typeof thematicsListQuery
+ | typeof thematicsSlugQuery
+ | typeof topicBySlugQuery
+ | typeof topicsListQuery
+ | typeof topicsSlugQuery
+ | typeof totalArticlesQuery
+ | typeof totalThematicsQuery
+ | typeof totalTopicsQuery;
- return new GraphQLClient(apiUrl);
+export type ArticleResponse<T> = {
+ post: T;
};
-export const fetchApi = async <T extends RequestType>(
- query: string,
- variables: VariablesType<T>
-): Promise<T> => {
- const client = getGraphQLClient();
+export type ArticlesResponse<T> = {
+ posts: T;
+};
+
+export type CommentsResponse<T> = {
+ comments: T;
+};
+
+export type SendCommentResponse<T> = {
+ createComment: T;
+};
+
+export type SendMailResponse<T> = {
+ sendEmail: T;
+};
- try {
- return await client.request(query, variables);
- } catch (error) {
- console.error(error, undefined, 2);
- process.exit(1);
+export type ThematicResponse<T> = {
+ thematic: T;
+};
+
+export type ThematicsResponse<T> = {
+ thematics: T;
+};
+
+export type TopicResponse<T> = {
+ topic: T;
+};
+
+export type TopicsResponse<T> = {
+ topics: T;
+};
+
+export type PageInfo = {
+ endCursor: string;
+ hasNextPage: boolean;
+ total: number;
+};
+
+export type Edges<T> = {
+ cursor: string;
+ node: T;
+};
+
+export type EdgesResponse<T> = {
+ edges: Edges<T>[];
+ pageInfo: PageInfo;
+};
+
+export type NodeResponse<T> = {
+ node: T;
+};
+
+export type NodesResponse<T> = {
+ nodes: T[];
+};
+
+export type EndCursor = Pick<
+ EdgesResponse<Pick<PageInfo, 'endCursor'>>,
+ 'pageInfo'
+>;
+
+export type ResponseMap<T> = {
+ [articleBySlugQuery]: ArticleResponse<T>;
+ [articlesCardQuery]: ArticlesResponse<NodesResponse<T>>;
+ [articlesEndCursor]: ArticlesResponse<EndCursor>;
+ [articlesQuery]: ArticlesResponse<EdgesResponse<T>>;
+ [articlesSlugQuery]: ArticlesResponse<EdgesResponse<T>>;
+ [commentsQuery]: CommentsResponse<NodesResponse<T>>;
+ [sendCommentMutation]: SendCommentResponse<T>;
+ [sendMailMutation]: SendMailResponse<T>;
+ [thematicBySlugQuery]: ThematicResponse<T>;
+ [thematicsListQuery]: ThematicsResponse<EdgesResponse<T>>;
+ [thematicsSlugQuery]: ThematicsResponse<EdgesResponse<T>>;
+ [topicBySlugQuery]: TopicResponse<T>;
+ [topicsListQuery]: TopicsResponse<EdgesResponse<T>>;
+ [topicsSlugQuery]: TopicsResponse<EdgesResponse<T>>;
+ [totalArticlesQuery]: ArticlesResponse<T>;
+ [totalThematicsQuery]: ThematicsResponse<T>;
+ [totalTopicsQuery]: TopicsResponse<T>;
+};
+
+export type GraphQLResponse<
+ T extends keyof ResponseMap<U>,
+ U
+> = ResponseMap<U>[T];
+
+export type BySlugVar = {
+ /**
+ * A slug.
+ */
+ slug: string;
+};
+
+export type EdgesVars = {
+ /**
+ * A cursor.
+ */
+ after?: string;
+ /**
+ * The number of items to return.
+ */
+ first: number;
+ /**
+ * A search query.
+ */
+ search?: string;
+};
+
+export type ByContentIdVar = {
+ /**
+ * An article id.
+ */
+ contentId: number;
+};
+
+export type SearchVar = {
+ /**
+ * A search term.
+ */
+ search?: string;
+};
+
+export type SendCommentVars = {
+ /**
+ * The author name.
+ */
+ author: string;
+ /**
+ * The author e-mail address.
+ */
+ authorEmail: string;
+ /**
+ * The author website.
+ */
+ authorUrl: string;
+ /**
+ * A mutation id.
+ */
+ clientMutationId: string;
+ /**
+ * A post or page id.
+ */
+ commentOn: number;
+ /**
+ * The comment body.
+ */
+ content: string;
+ /**
+ * The comment parent.
+ */
+ parent?: number;
+};
+
+export type SendMailVars = {
+ /**
+ * The mail body.
+ */
+ body: string;
+ /**
+ * A mutation id.
+ */
+ clientMutationId: string;
+ /**
+ * The reply to e-mail address.
+ */
+ replyTo: string;
+ /**
+ * The mail subject.
+ */
+ subject: string;
+};
+
+export type VariablesMap = {
+ [articleBySlugQuery]: BySlugVar;
+ [articlesCardQuery]: EdgesVars;
+ [articlesEndCursor]: EdgesVars;
+ [articlesQuery]: EdgesVars;
+ [articlesSlugQuery]: EdgesVars;
+ [commentsQuery]: ByContentIdVar;
+ [sendCommentMutation]: SendCommentVars;
+ [sendMailMutation]: SendMailVars;
+ [thematicBySlugQuery]: BySlugVar;
+ [thematicsListQuery]: EdgesVars;
+ [thematicsSlugQuery]: EdgesVars;
+ [topicBySlugQuery]: BySlugVar;
+ [topicsListQuery]: EdgesVars;
+ [topicsSlugQuery]: EdgesVars;
+ [totalArticlesQuery]: SearchVar;
+ [totalThematicsQuery]: null;
+ [totalTopicsQuery]: null;
+};
+
+export type FetchAPIProps<T extends Queries | Mutations> = {
+ /**
+ * A GraphQL API URL.
+ */
+ api: string;
+ /**
+ * A GraphQL query.
+ */
+ query: T;
+ /**
+ * (Optional) The query variables.
+ */
+ variables?: VariablesMap[T];
+};
+
+/**
+ * Fetch a GraphQL API.
+ * @param {object} obj - An object.
+ * @param {string} obj.api - A GraphQL API URL.
+ * @param {Queries} obj.query - A GraphQL query.
+ * @param {object} [obj.variables] - The query variables.
+ */
+export async function fetchAPI<T, U extends Queries | Mutations>({
+ api,
+ query,
+ variables,
+}: FetchAPIProps<U>): Promise<GraphQLResponse<U, T>> {
+ const response = await fetch(api, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json;charset=UTF-8',
+ },
+ body: JSON.stringify({
+ query,
+ variables,
+ }),
+ });
+
+ type JSONResponse = {
+ data?: GraphQLResponse<U, T>;
+ errors?: Array<{ message: string }>;
+ };
+
+ const { data, errors }: JSONResponse = await response.json();
+
+ if (response.ok) {
+ if (!data) return Promise.reject(new Error(`No data found"`));
+
+ return data;
+ } else {
+ console.error('Failed to fetch API');
+ const error = new Error(
+ errors?.map((e) => e.message).join('\n') ?? 'unknown'
+ );
+ return Promise.reject(error);
+ }
+}
+
+/**
+ * Retrieve the API url from settings.
+ *
+ * @returns {string} The API url.
+ */
+export const getAPIUrl = (): string => {
+ const { url } = settings.api;
+
+ if (!url) {
+ throw new Error('API url is not defined.');
}
+
+ return url;
};
diff --git a/src/services/graphql/articles.query.ts b/src/services/graphql/articles.query.ts
new file mode 100644
index 0000000..3e1f575
--- /dev/null
+++ b/src/services/graphql/articles.query.ts
@@ -0,0 +1,191 @@
+/**
+ * Query the full article data using its slug.
+ */
+export const articleBySlugQuery = `query PostBy($slug: ID!) {
+ post(id: $slug, idType: SLUG) {
+ acfPosts {
+ postsInThematic {
+ ... on Thematic {
+ databaseId
+ slug
+ title
+ }
+ }
+ postsInTopic {
+ ... on Topic {
+ databaseId
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+ }
+ author {
+ node {
+ gravatarUrl
+ name
+ url
+ }
+ }
+ commentCount
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ seo {
+ metaDesc
+ title
+ }
+ slug
+ title
+ }
+}`;
+
+/**
+ * Query an array of partial articles.
+ */
+export const articlesQuery = `query Articles($after: String = "", $first: Int = 10, $search: String = "") {
+ posts(
+ after: $after
+ first: $first
+ where: {orderby: {field: DATE, order: DESC}, search: $search, status: PUBLISH}
+ ) {
+ edges {
+ cursor
+ node {
+ acfPosts {
+ postsInThematic {
+ ... on Thematic {
+ databaseId
+ slug
+ title
+ }
+ }
+ }
+ commentCount
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ slug
+ title
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ total
+ }
+ }
+}`;
+
+/**
+ * Query an array of articles with only the minimal data.
+ */
+export const articlesCardQuery = `query ArticlesCard($first: Int = 10) {
+ posts(
+ first: $first
+ where: {orderby: {field: DATE, order: DESC}, status: PUBLISH}
+ ) {
+ nodes {
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+}`;
+
+/**
+ * Query an array of articles slug.
+ */
+export const articlesSlugQuery = `query ArticlesSlug($first: Int = 10, $after: String = "") {
+ posts(after: $after, first: $first) {
+ edges {
+ cursor
+ node {
+ slug
+ }
+ }
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Query the total number of articles.
+ */
+export const totalArticlesQuery = `query PostsTotal($search: String = "") {
+ posts(where: {search: $search}) {
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Query the end cursor based on the queried posts number.
+ */
+export const articlesEndCursor = `query EndCursorAfter($first: Int) {
+ posts(first: $first) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ }
+}`;
diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts
new file mode 100644
index 0000000..27406ac
--- /dev/null
+++ b/src/services/graphql/articles.ts
@@ -0,0 +1,200 @@
+import { Slug, type Article, type ArticleCard } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawArticlePreview,
+ type TotalItems,
+} from '@ts/types/raw-data';
+import { getAuthorFromRawData } from '@utils/helpers/author';
+import { getImageFromRawData } from '@utils/helpers/images';
+import { getPageLinkFromRawData } from '@utils/helpers/pages';
+import {
+ EdgesResponse,
+ EdgesVars,
+ EndCursor,
+ fetchAPI,
+ getAPIUrl,
+ PageInfo,
+} from './api';
+import {
+ articleBySlugQuery,
+ articlesCardQuery,
+ articlesEndCursor,
+ articlesQuery,
+ articlesSlugQuery,
+ totalArticlesQuery,
+} from './articles.query';
+
+/**
+ * Retrieve the total number of articles.
+ *
+ * @returns {Promise<number>} - The articles total number.
+ */
+export const getTotalArticles = async (search?: string): Promise<number> => {
+ const response = await fetchAPI<TotalItems, typeof totalArticlesQuery>({
+ api: getAPIUrl(),
+ query: totalArticlesQuery,
+ variables: { search },
+ });
+
+ return response.posts.pageInfo.total;
+};
+
+export type GetArticlesReturn = {
+ articles: Article[];
+ pageInfo: PageInfo;
+};
+
+/**
+ * Convert raw data to an Article object.
+ *
+ * @param {RawArticle} data - The page raw data.
+ * @returns {Article} The page data.
+ */
+export const getArticleFromRawData = (data: RawArticle): Article => {
+ const {
+ acfPosts,
+ author,
+ commentCount,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ info,
+ modified,
+ slug,
+ title,
+ seo,
+ } = data;
+
+ return {
+ content: contentParts.afterMore,
+ id: databaseId,
+ intro: contentParts.beforeMore,
+ meta: {
+ author: author && getAuthorFromRawData(author.node, 'page'),
+ commentsCount: commentCount || 0,
+ cover: featuredImage?.node
+ ? getImageFromRawData(featuredImage.node)
+ : undefined,
+ dates: { publication: date, update: modified },
+ seo: {
+ description: seo?.metaDesc || '',
+ title: seo?.title || '',
+ },
+ thematics: acfPosts.postsInThematic?.map((thematic) =>
+ getPageLinkFromRawData(thematic, 'thematic')
+ ),
+ topics: acfPosts.postsInTopic?.map((topic) =>
+ getPageLinkFromRawData(topic, 'topic')
+ ),
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
+
+/**
+ * Retrieve the given number of articles from API.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<EdgesResponse<RawArticle>>} The articles data.
+ */
+export const getArticles = async (
+ props: EdgesVars
+): Promise<EdgesResponse<RawArticle>> => {
+ const response = await fetchAPI<RawArticle, typeof articlesQuery>({
+ api: getAPIUrl(),
+ query: articlesQuery,
+ variables: { ...props },
+ });
+
+ return response.posts;
+};
+
+/**
+ * Convert a raw article preview to an article card.
+ *
+ * @param {RawArticlePreview} data - A raw article preview.
+ * @returns {ArticleCard} An article card.
+ */
+const getArticleCardFromRawData = (data: RawArticlePreview): ArticleCard => {
+ const { databaseId, date, featuredImage, slug, title } = data;
+
+ return {
+ cover: featuredImage ? getImageFromRawData(featuredImage.node) : undefined,
+ dates: { publication: date },
+ id: databaseId,
+ slug,
+ title,
+ };
+};
+
+/**
+ * Retrieve the given number of article cards from API.
+ *
+ * @param {EdgesVars} obj - An object.
+ * @param {number} obj.first - The number of articles.
+ * @returns {Promise<ArticleCard[]>} - The article cards data.
+ */
+export const getArticlesCard = async ({
+ first,
+}: EdgesVars): Promise<ArticleCard[]> => {
+ const response = await fetchAPI<RawArticlePreview, typeof articlesCardQuery>({
+ api: getAPIUrl(),
+ query: articlesCardQuery,
+ variables: { first },
+ });
+
+ return response.posts.nodes.map((node) => getArticleCardFromRawData(node));
+};
+
+/**
+ * Retrieve an Article object by slug.
+ *
+ * @param {string} slug - The article slug.
+ * @returns {Promise<Article>} The requested article.
+ */
+export const getArticleBySlug = async (slug: string): Promise<Article> => {
+ const response = await fetchAPI<RawArticle, typeof articleBySlugQuery>({
+ api: getAPIUrl(),
+ query: articleBySlugQuery,
+ variables: { slug },
+ });
+
+ return getArticleFromRawData(response.post);
+};
+
+/**
+ * Retrieve all the articles slugs.
+ *
+ * @returns {Promise<string[]>} - An array of articles slugs.
+ */
+export const getAllArticlesSlugs = async (): Promise<string[]> => {
+ const totalArticles = await getTotalArticles();
+ const response = await fetchAPI<Slug, typeof articlesSlugQuery>({
+ api: getAPIUrl(),
+ query: articlesSlugQuery,
+ variables: { first: totalArticles },
+ });
+
+ return response.posts.edges.map((edge) => edge.node.slug);
+};
+
+/**
+ * Retrieve the last cursor.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<string>} - The end cursor.
+ */
+export const getArticlesEndCursor = async (
+ props: EdgesVars
+): Promise<string> => {
+ const response = await fetchAPI<EndCursor, typeof articlesEndCursor>({
+ api: getAPIUrl(),
+ query: articlesEndCursor,
+ variables: { ...props },
+ });
+
+ return response.posts.pageInfo.endCursor;
+};
diff --git a/src/services/graphql/comments.mutation.ts b/src/services/graphql/comments.mutation.ts
new file mode 100644
index 0000000..f52c7e9
--- /dev/null
+++ b/src/services/graphql/comments.mutation.ts
@@ -0,0 +1,30 @@
+/**
+ * Send comment mutation.
+ */
+export const sendCommentMutation = `mutation CreateComment(
+ $author: String!
+ $authorEmail: String!
+ $authorUrl: String!
+ $content: String!
+ $parent: ID = null
+ $commentOn: Int!
+ $clientMutationId: String!
+) {
+ createComment(
+ input: {
+ author: $author
+ authorEmail: $authorEmail
+ authorUrl: $authorUrl
+ content: $content
+ parent: $parent
+ commentOn: $commentOn
+ clientMutationId: $clientMutationId
+ }
+ ) {
+ clientMutationId
+ success
+ comment {
+ approved
+ }
+ }
+}`;
diff --git a/src/services/graphql/comments.query.ts b/src/services/graphql/comments.query.ts
new file mode 100644
index 0000000..ef93e89
--- /dev/null
+++ b/src/services/graphql/comments.query.ts
@@ -0,0 +1,21 @@
+/**
+ * Query the comments data by post id.
+ */
+export const commentsQuery = `query CommentsByPostId($contentId: ID!) {
+ comments(where: {contentId: $contentId, order: ASC, orderby: COMMENT_DATE}) {
+ nodes {
+ approved
+ author {
+ node {
+ gravatarUrl
+ name
+ url
+ }
+ }
+ content
+ databaseId
+ date
+ parentDatabaseId
+ }
+ }
+}`;
diff --git a/src/services/graphql/comments.ts b/src/services/graphql/comments.ts
new file mode 100644
index 0000000..28ddfd0
--- /dev/null
+++ b/src/services/graphql/comments.ts
@@ -0,0 +1,102 @@
+import { Comment } from '@ts/types/app';
+import { RawComment } from '@ts/types/raw-data';
+import { getAuthorFromRawData } from '@utils/helpers/author';
+import { fetchAPI, getAPIUrl, SendCommentVars } from './api';
+import { sendCommentMutation } from './comments.mutation';
+import { commentsQuery } from './comments.query';
+
+/**
+ * Create a comments tree with replies.
+ *
+ * @param {Comment[]} comments - A flatten comments list.
+ * @returns {Comment[]} An array of comments with replies.
+ */
+export const buildCommentsTree = (comments: Comment[]): Comment[] => {
+ type CommentsHashTable = {
+ [key: string]: Comment;
+ };
+
+ const hashTable: CommentsHashTable = Object.create(null);
+ const commentsTree: Comment[] = [];
+
+ comments.forEach(
+ (comment) => (hashTable[comment.id] = { ...comment, replies: [] })
+ );
+
+ comments.forEach((comment) => {
+ if (!comment.parentId) {
+ commentsTree.push(hashTable[comment.id]);
+ } else {
+ hashTable[comment.parentId].replies.push(hashTable[comment.id]);
+ }
+ });
+
+ return commentsTree;
+};
+
+/**
+ * Convert a comment from RawComment to Comment type.
+ *
+ * @param {RawComment} comment - A raw comment.
+ * @returns {Comment} A formatted comment.
+ */
+export const getCommentFromRawData = (comment: RawComment): Comment => {
+ const { author, databaseId, date, parentDatabaseId, ...data } = comment;
+
+ return {
+ id: databaseId,
+ meta: {
+ author: getAuthorFromRawData(author.node, 'comment'),
+ date,
+ },
+ parentId: parentDatabaseId,
+ replies: [],
+ ...data,
+ };
+};
+
+/**
+ * Retrieve a comments list by post id.
+ *
+ * @param {number} id - A post id.
+ * @returns {Promise<Comment[]>} The comments list.
+ */
+export const getPostComments = async (id: number): Promise<Comment[]> => {
+ const response = await fetchAPI<RawComment, typeof commentsQuery>({
+ api: getAPIUrl(),
+ query: commentsQuery,
+ variables: { contentId: id },
+ });
+
+ const comments = response.comments.nodes.map((comment) =>
+ getCommentFromRawData(comment)
+ );
+
+ return buildCommentsTree(comments);
+};
+
+export type SentComment = {
+ clientMutationId: string;
+ success: boolean;
+ comment: {
+ approved: boolean;
+ } | null;
+};
+
+/**
+ * Send a comment using GraphQL API.
+ *
+ * @param {SendCommentVars} data - The comment data.
+ * @returns {Promise<SentEmail>} The mutation response.
+ */
+export const sendComment = async (
+ data: SendCommentVars
+): Promise<SentComment> => {
+ const response = await fetchAPI<SentComment, typeof sendCommentMutation>({
+ api: getAPIUrl(),
+ query: sendCommentMutation,
+ variables: { ...data },
+ });
+
+ return response.createComment;
+};
diff --git a/src/services/graphql/contact.mutation.ts b/src/services/graphql/contact.mutation.ts
new file mode 100644
index 0000000..b82fc07
--- /dev/null
+++ b/src/services/graphql/contact.mutation.ts
@@ -0,0 +1,25 @@
+/**
+ * Send mail mutation.
+ */
+export const sendMailMutation = `mutation SendEmail(
+ $subject: String!
+ $body: String!
+ $replyTo: String!
+ $clientMutationId: String!
+) {
+ sendEmail(
+ input: {
+ clientMutationId: $clientMutationId
+ body: $body
+ replyTo: $replyTo
+ subject: $subject
+ }
+ ) {
+ clientMutationId
+ message
+ sent
+ origin
+ replyTo
+ to
+ }
+}`;
diff --git a/src/services/graphql/contact.ts b/src/services/graphql/contact.ts
new file mode 100644
index 0000000..00c6ca2
--- /dev/null
+++ b/src/services/graphql/contact.ts
@@ -0,0 +1,26 @@
+import { fetchAPI, getAPIUrl, SendMailVars } from './api';
+import { sendMailMutation } from './contact.mutation';
+
+export type SentEmail = {
+ clientMutationId: string;
+ message: string;
+ origin: string;
+ replyTo: string;
+ sent: boolean;
+};
+
+/**
+ * Send an email using GraphQL API.
+ *
+ * @param {sendMailVars} data - The mail data.
+ * @returns {Promise<SentEmail>} The mutation response.
+ */
+export const sendMail = async (data: SendMailVars): Promise<SentEmail> => {
+ const response = await fetchAPI<SentEmail, typeof sendMailMutation>({
+ api: getAPIUrl(),
+ query: sendMailMutation,
+ variables: { ...data },
+ });
+
+ return response.sendEmail;
+};
diff --git a/src/services/graphql/mutations.ts b/src/services/graphql/mutations.ts
deleted file mode 100644
index c697835..0000000
--- a/src/services/graphql/mutations.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { CommentData, CreateComment, CreatedComment } from '@ts/types/comments';
-import { ContactData, SendEmail } from '@ts/types/contact';
-import { gql } from 'graphql-request';
-import { fetchApi } from './api';
-
-//==============================================================================
-// Comment mutation
-//==============================================================================
-
-export const createComment = async (
- data: CommentData
-): Promise<CreatedComment> => {
- const mutation = gql`
- mutation CreateComment(
- $author: String!
- $authorEmail: String!
- $authorUrl: String!
- $content: String!
- $parent: ID!
- $commentOn: Int!
- $mutationId: String!
- ) {
- createComment(
- input: {
- author: $author
- authorEmail: $authorEmail
- authorUrl: $authorUrl
- content: $content
- parent: $parent
- commentOn: $commentOn
- clientMutationId: $mutationId
- }
- ) {
- clientMutationId
- success
- comment {
- approved
- }
- }
- }
- `;
-
- const variables = { ...data };
- const response = await fetchApi<CreateComment>(mutation, variables);
-
- return response.createComment;
-};
-
-//==============================================================================
-// Contact mutation
-//==============================================================================
-
-export const sendMail = async (data: ContactData) => {
- const mutation = gql`
- mutation SendEmail(
- $subject: String!
- $body: String!
- $replyTo: String!
- $mutationId: String!
- ) {
- sendEmail(
- input: {
- clientMutationId: $mutationId
- body: $body
- replyTo: $replyTo
- subject: $subject
- }
- ) {
- clientMutationId
- message
- sent
- origin
- replyTo
- to
- }
- }
- `;
-
- const variables = { ...data };
- const response = await fetchApi<SendEmail>(mutation, variables);
- return response.sendEmail;
-};
diff --git a/src/services/graphql/queries.ts b/src/services/graphql/queries.ts
deleted file mode 100644
index 9caf62b..0000000
--- a/src/services/graphql/queries.ts
+++ /dev/null
@@ -1,535 +0,0 @@
-import { Slug } from '@ts/types/app';
-import { Article, PostBy, TotalArticles } from '@ts/types/articles';
-import {
- AllPostsSlug,
- LastPostCursor,
- PostsList,
- RawPostsList,
-} from '@ts/types/blog';
-import { Comment, CommentsByPostId } from '@ts/types/comments';
-import {
- AllTopics,
- AllTopicsSlug,
- AllThematics,
- AllThematicsSlug,
- Topic,
- TopicBy,
- TopicPreview,
- Thematic,
- ThematicBy,
- ThematicPreview,
-} from '@ts/types/taxonomies';
-import {
- getFormattedPost,
- getFormattedPostPreview,
- getFormattedTopic,
- getFormattedThematic,
- getFormattedComments,
- buildCommentsTree,
-} from '@utils/helpers/format';
-import { gql } from 'graphql-request';
-import { fetchApi } from './api';
-
-//==============================================================================
-// Posts list queries
-//==============================================================================
-
-export const getPostsTotal = async (): Promise<number> => {
- const query = gql`
- query PostsTotal {
- posts {
- pageInfo {
- total
- }
- }
- }
- `;
-
- const response = await fetchApi<TotalArticles>(query, null);
- return response.posts.pageInfo.total;
-};
-
-export const getPublishedPosts = async ({
- first = 10,
- after = '',
- searchQuery = '',
-}: {
- first: number;
- after?: string;
- searchQuery?: string;
-}): Promise<PostsList> => {
- const query = gql`
- query AllPublishedPosts($first: Int, $after: String, $searchQuery: String) {
- posts(
- after: $after
- first: $first
- where: {
- status: PUBLISH
- orderby: { field: DATE, order: DESC }
- search: $searchQuery
- }
- ) {
- edges {
- cursor
- node {
- acfPosts {
- postsInTopic {
- ... on Topic {
- databaseId
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- id
- slug
- title
- }
- }
- postsInThematic {
- ... on Thematic {
- databaseId
- id
- slug
- title
- }
- }
- }
- commentCount
- contentParts {
- beforeMore
- }
- date
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- id
- info {
- readingTime
- wordsCount
- }
- databaseId
- modified
- slug
- title
- }
- }
- pageInfo {
- endCursor
- hasNextPage
- total
- }
- }
- }
- `;
-
- const variables = { first, after, searchQuery };
- const response = await fetchApi<RawPostsList>(query, variables);
- const formattedPosts = response.posts.edges.map((post) => {
- return getFormattedPostPreview(post.node);
- });
-
- return {
- posts: formattedPosts,
- pageInfo: response.posts.pageInfo,
- };
-};
-
-export const getAllPostsSlug = async (): Promise<Slug[]> => {
- // 10 000 is an arbitrary number that I use for small websites.
- const query = gql`
- query AllPostsSlug {
- posts(first: 10000) {
- nodes {
- slug
- }
- }
- }
- `;
-
- const response = await fetchApi<AllPostsSlug>(query, null);
- return response.posts.nodes;
-};
-
-//==============================================================================
-// Single Post query
-//==============================================================================
-
-export const getPostBySlug = async (slug: string): Promise<Article> => {
- const query = gql`
- query PostBySlug($slug: ID!) {
- post(id: $slug, idType: SLUG) {
- acfPosts {
- postsInTopic {
- ... on Topic {
- id
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- slug
- title
- }
- }
- postsInThematic {
- ... on Thematic {
- id
- slug
- title
- }
- }
- }
- author {
- node {
- firstName
- lastName
- name
- }
- }
- commentCount
- contentParts {
- afterMore
- beforeMore
- }
- databaseId
- date
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- id
- info {
- readingTime
- wordsCount
- }
- modified
- seo {
- metaDesc
- title
- }
- title
- }
- }
- `;
- const variables = { slug };
- const response = await fetchApi<PostBy>(query, variables);
-
- return getFormattedPost(response.post);
-};
-
-//==============================================================================
-// Comments query
-//==============================================================================
-
-export const getCommentsByPostId = async (id: number): Promise<Comment[]> => {
- const query = gql`
- query PostComments($id: ID!) {
- comments(where: { contentId: $id, order: ASC, orderby: COMMENT_DATE }) {
- nodes {
- approved
- author {
- node {
- databaseId
- gravatarUrl
- name
- url
- }
- }
- content
- databaseId
- date
- parentDatabaseId
- }
- }
- }
- `;
-
- const variables = { id };
- const response = await fetchApi<CommentsByPostId>(query, variables);
- const formattedComments = getFormattedComments(response.comments.nodes);
-
- return buildCommentsTree(formattedComments);
-};
-
-//==============================================================================
-// Topic query
-//==============================================================================
-
-export const getTopicBySlug = async (slug: string): Promise<Topic> => {
- const query = gql`
- query TopicBySlug($slug: ID!) {
- topic(id: $slug, idType: SLUG) {
- acfTopics {
- officialWebsite
- postsInTopic {
- ... on Post {
- acfPosts {
- postsInTopic {
- ... on Topic {
- databaseId
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- id
- slug
- title
- }
- }
- postsInThematic {
- ... on Thematic {
- databaseId
- id
- slug
- title
- }
- }
- }
- id
- info {
- readingTime
- wordsCount
- }
- commentCount
- contentParts {
- beforeMore
- }
- databaseId
- date
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- modified
- slug
- title
- }
- }
- }
- contentParts {
- afterMore
- beforeMore
- }
- databaseId
- date
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- id
- info {
- readingTime
- wordsCount
- }
- modified
- seo {
- metaDesc
- title
- }
- title
- }
- }
- `;
- const variables = { slug };
- const response = await fetchApi<TopicBy>(query, variables);
-
- return getFormattedTopic(response.topic);
-};
-
-export const getAllTopicsSlug = async (): Promise<Slug[]> => {
- // 10 000 is an arbitrary number that I use for small websites.
- const query = gql`
- query AllTopicsSlug {
- topics(first: 10000) {
- nodes {
- slug
- }
- }
- }
- `;
- const response = await fetchApi<AllTopicsSlug>(query, null);
- return response.topics.nodes;
-};
-
-export const getAllTopics = async (): Promise<TopicPreview[]> => {
- // 10 000 is an arbitrary number that I use for small websites.
- const query = gql`
- query AllTopics {
- topics(first: 10000, where: { orderby: { field: TITLE, order: ASC } }) {
- nodes {
- databaseId
- slug
- title
- }
- }
- }
- `;
-
- const response = await fetchApi<AllTopics>(query, null);
- return response.topics.nodes;
-};
-
-//==============================================================================
-// Thematic query
-//==============================================================================
-
-export const getThematicBySlug = async (slug: string): Promise<Thematic> => {
- const query = gql`
- query ThematicBySlug($slug: ID!) {
- thematic(id: $slug, idType: SLUG) {
- acfThematics {
- postsInThematic {
- ... on Post {
- acfPosts {
- postsInTopic {
- ... on Topic {
- databaseId
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- id
- slug
- title
- }
- }
- postsInThematic {
- ... on Thematic {
- databaseId
- id
- slug
- title
- }
- }
- }
- id
- info {
- readingTime
- wordsCount
- }
- commentCount
- contentParts {
- beforeMore
- }
- databaseId
- date
- featuredImage {
- node {
- altText
- sourceUrl
- title
- }
- }
- modified
- slug
- title
- }
- }
- }
- contentParts {
- afterMore
- beforeMore
- }
- databaseId
- date
- id
- info {
- readingTime
- wordsCount
- }
- modified
- seo {
- metaDesc
- title
- }
- title
- }
- }
- `;
- const variables = { slug };
- const response = await fetchApi<ThematicBy>(query, variables);
-
- return getFormattedThematic(response.thematic);
-};
-
-export const getAllThematicsSlug = async (): Promise<Slug[]> => {
- // 10 000 is an arbitrary number that I use for small websites.
- const query = gql`
- query AllThematicsSlug {
- thematics(first: 10000) {
- nodes {
- slug
- }
- }
- }
- `;
- const response = await fetchApi<AllThematicsSlug>(query, null);
- return response.thematics.nodes;
-};
-
-export const getAllThematics = async (): Promise<ThematicPreview[]> => {
- // 10 000 is an arbitrary number that I use for small websites.
- const query = gql`
- query AllThematics {
- thematics(
- first: 10000
- where: { orderby: { field: TITLE, order: ASC } }
- ) {
- nodes {
- databaseId
- slug
- title
- }
- }
- }
- `;
-
- const response = await fetchApi<AllThematics>(query, null);
- return response.thematics.nodes;
-};
-
-export const getEndCursor = async ({
- first = 10,
-}: {
- first: number;
-}): Promise<string> => {
- const query = gql`
- query EndCursorAfter($first: Int) {
- posts(first: $first) {
- pageInfo {
- hasNextPage
- endCursor
- }
- }
- }
- `;
-
- const variables = { first };
- const response = await fetchApi<LastPostCursor>(query, variables);
-
- return response.posts.pageInfo.endCursor;
-};
diff --git a/src/services/graphql/thematics.query.ts b/src/services/graphql/thematics.query.ts
new file mode 100644
index 0000000..5a82133
--- /dev/null
+++ b/src/services/graphql/thematics.query.ts
@@ -0,0 +1,125 @@
+/**
+ * Query the full thematic data using its slug.
+ */
+export const thematicBySlugQuery = `query ThematicBy($slug: ID!) {
+ thematic(id: $slug, idType: SLUG) {
+ acfThematics {
+ postsInThematic {
+ ... on Post {
+ acfPosts {
+ postsInTopic {
+ ... on Topic {
+ databaseId
+ slug
+ title
+ }
+ }
+ }
+ commentCount
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ slug
+ title
+ }
+ }
+ }
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ seo {
+ metaDesc
+ title
+ }
+ slug
+ title
+ }
+}`;
+
+/**
+ * Query an array of partial thematics.
+ */
+export const thematicsListQuery = `query ThematicsList($after: String = "", $first: Int = 10) {
+ thematics(
+ after: $after
+ first: $first
+ where: {orderby: {field: TITLE, order: ASC}, status: PUBLISH}
+ ) {
+ edges {
+ cursor
+ node {
+ databaseId
+ slug
+ title
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ total
+ }
+ }
+}`;
+
+/**
+ * Query an array of thematics slug.
+ */
+export const thematicsSlugQuery = `query ThematicsSlug($first: Int = 10, $after: String = "") {
+ thematics(after: $after, first: $first) {
+ edges {
+ cursor
+ node {
+ slug
+ }
+ }
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Query the total number of thematics.
+ */
+export const totalThematicsQuery = `query ThematicsTotal {
+ thematics {
+ pageInfo {
+ total
+ }
+ }
+}`;
diff --git a/src/services/graphql/thematics.ts b/src/services/graphql/thematics.ts
new file mode 100644
index 0000000..4dc69e7
--- /dev/null
+++ b/src/services/graphql/thematics.ts
@@ -0,0 +1,162 @@
+import { PageLink, Slug, Thematic } from '@ts/types/app';
+import {
+ RawArticle,
+ RawThematic,
+ RawThematicPreview,
+ TotalItems,
+} from '@ts/types/raw-data';
+import { getImageFromRawData } from '@utils/helpers/images';
+import { getPageLinkFromRawData } from '@utils/helpers/pages';
+import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl } from './api';
+import { getArticleFromRawData } from './articles';
+import {
+ thematicBySlugQuery,
+ thematicsListQuery,
+ thematicsSlugQuery,
+ totalThematicsQuery,
+} from './thematics.query';
+
+/**
+ * Retrieve the total number of thematics.
+ *
+ * @returns {Promise<number>} - The thematics total number.
+ */
+export const getTotalThematics = async (): Promise<number> => {
+ const response = await fetchAPI<TotalItems, typeof totalThematicsQuery>({
+ api: getAPIUrl(),
+ query: totalThematicsQuery,
+ });
+
+ return response.thematics.pageInfo.total;
+};
+
+/**
+ * Retrieve the given number of thematics from API.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<EdgesResponse<RawThematicPreview>>} The thematics data.
+ */
+export const getThematicsPreview = async (
+ props: EdgesVars
+): Promise<EdgesResponse<RawThematicPreview>> => {
+ const response = await fetchAPI<
+ RawThematicPreview,
+ typeof thematicsListQuery
+ >({ api: getAPIUrl(), query: thematicsListQuery, variables: props });
+
+ return response.thematics;
+};
+
+/**
+ * Convert raw data to an Thematic object.
+ *
+ * @param {RawThematic} data - The page raw data.
+ * @returns {Thematic} The page data.
+ */
+export const getThematicFromRawData = (data: RawThematic): Thematic => {
+ const {
+ acfThematics,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ info,
+ modified,
+ slug,
+ title,
+ seo,
+ } = data;
+
+ /**
+ * Retrieve an array of related topics.
+ *
+ * @param posts - The thematic posts.
+ * @returns {PageLink[]} An array of topics links.
+ */
+ const getRelatedTopics = (posts: RawArticle[]): PageLink[] => {
+ const topics: PageLink[] = [];
+
+ posts.forEach((post) => {
+ if (post.acfPosts.postsInTopic) {
+ post.acfPosts.postsInTopic.forEach((topic) =>
+ topics.push(getPageLinkFromRawData(topic, 'topic'))
+ );
+ }
+ });
+
+ const topicsIds = topics.map((topic) => topic.id);
+ const uniqueTopics = topics.filter(
+ ({ id }, index) => !topicsIds.includes(id, index + 1)
+ );
+ const sortTopicByName = (a: PageLink, b: PageLink) => {
+ var nameA = a.name.toUpperCase(); // ignore upper and lowercase
+ var nameB = b.name.toUpperCase(); // ignore upper and lowercase
+ if (nameA < nameB) {
+ return -1;
+ }
+ if (nameA > nameB) {
+ return 1;
+ }
+
+ // names must be equal
+ return 0;
+ };
+
+ return uniqueTopics.sort(sortTopicByName);
+ };
+
+ return {
+ content: contentParts.afterMore,
+ id: databaseId,
+ intro: contentParts.beforeMore,
+ meta: {
+ articles: acfThematics.postsInThematic.map((post) =>
+ getArticleFromRawData(post)
+ ),
+ cover: featuredImage?.node
+ ? getImageFromRawData(featuredImage.node)
+ : undefined,
+ dates: { publication: date, update: modified },
+ seo: {
+ description: seo?.metaDesc || '',
+ title: seo?.title || '',
+ },
+ topics: getRelatedTopics(acfThematics.postsInThematic),
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
+
+/**
+ * Retrieve a Thematic object by slug.
+ *
+ * @param {string} slug - The thematic slug.
+ * @returns {Promise<Article>} The requested thematic.
+ */
+export const getThematicBySlug = async (slug: string): Promise<Thematic> => {
+ const response = await fetchAPI<RawThematic, typeof thematicBySlugQuery>({
+ api: getAPIUrl(),
+ query: thematicBySlugQuery,
+ variables: { slug },
+ });
+
+ return getThematicFromRawData(response.thematic);
+};
+
+/**
+ * Retrieve all the thematics slugs.
+ *
+ * @returns {Promise<string[]>} - An array of thematics slugs.
+ */
+export const getAllThematicsSlugs = async (): Promise<string[]> => {
+ const totalThematics = await getTotalThematics();
+ const response = await fetchAPI<Slug, typeof thematicsSlugQuery>({
+ api: getAPIUrl(),
+ query: thematicsSlugQuery,
+ variables: { first: totalThematics },
+ });
+
+ return response.thematics.edges.map((edge) => edge.node.slug);
+};
diff --git a/src/services/graphql/topics.query.ts b/src/services/graphql/topics.query.ts
new file mode 100644
index 0000000..57b2569
--- /dev/null
+++ b/src/services/graphql/topics.query.ts
@@ -0,0 +1,137 @@
+/**
+ * Query the full topic data using its slug.
+ */
+export const topicBySlugQuery = `query TopicBy($slug: ID!) {
+ topic(id: $slug, idType: SLUG) {
+ acfTopics {
+ officialWebsite
+ postsInTopic {
+ ... on Post {
+ acfPosts {
+ postsInThematic {
+ ... on Thematic {
+ databaseId
+ slug
+ title
+ }
+ }
+ }
+ commentCount
+ contentParts {
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ slug
+ title
+ }
+ }
+ }
+ contentParts {
+ afterMore
+ beforeMore
+ }
+ databaseId
+ date
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ info {
+ wordsCount
+ }
+ modified
+ seo {
+ metaDesc
+ title
+ }
+ slug
+ title
+ }
+}`;
+
+/**
+ * Query an array of partial topics.
+ */
+export const topicsListQuery = `query TopicsList($after: String = "", $first: Int = 10) {
+ topics(
+ after: $after
+ first: $first
+ where: {orderby: {field: TITLE, order: ASC}, status: PUBLISH}
+ ) {
+ edges {
+ cursor
+ node {
+ databaseId
+ featuredImage {
+ node {
+ altText
+ mediaDetails {
+ height
+ width
+ }
+ sourceUrl
+ title
+ }
+ }
+ slug
+ title
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ total
+ }
+ }
+}`;
+
+/**
+ * Query an array of topics slug.
+ */
+export const topicsSlugQuery = `query TopicsSlug($first: Int = 10, $after: String = "") {
+ topics(after: $after, first: $first) {
+ edges {
+ cursor
+ node {
+ slug
+ }
+ }
+ pageInfo {
+ total
+ }
+ }
+}`;
+
+/**
+ * Query the total number of topics.
+ */
+export const totalTopicsQuery = `query TopicsTotal {
+ topics {
+ pageInfo {
+ total
+ }
+ }
+}`;
diff --git a/src/services/graphql/topics.ts b/src/services/graphql/topics.ts
new file mode 100644
index 0000000..0b1971b
--- /dev/null
+++ b/src/services/graphql/topics.ts
@@ -0,0 +1,164 @@
+import { PageLink, Slug, Topic } from '@ts/types/app';
+import {
+ RawArticle,
+ RawTopic,
+ RawTopicPreview,
+ TotalItems,
+} from '@ts/types/raw-data';
+import { getImageFromRawData } from '@utils/helpers/images';
+import { getPageLinkFromRawData } from '@utils/helpers/pages';
+import { EdgesResponse, EdgesVars, fetchAPI, getAPIUrl } from './api';
+import { getArticleFromRawData } from './articles';
+import {
+ topicBySlugQuery,
+ topicsListQuery,
+ topicsSlugQuery,
+ totalTopicsQuery,
+} from './topics.query';
+
+/**
+ * Retrieve the total number of topics.
+ *
+ * @returns {Promise<number>} - The topics total number.
+ */
+export const getTotalTopics = async (): Promise<number> => {
+ const response = await fetchAPI<TotalItems, typeof totalTopicsQuery>({
+ api: getAPIUrl(),
+ query: totalTopicsQuery,
+ });
+
+ return response.topics.pageInfo.total;
+};
+
+/**
+ * Retrieve the given number of topics from API.
+ *
+ * @param {EdgesVars} props - An object of GraphQL variables.
+ * @returns {Promise<EdgesResponse<RawTopicPreview>>} The topics data.
+ */
+export const getTopicsPreview = async (
+ props: EdgesVars
+): Promise<EdgesResponse<RawTopicPreview>> => {
+ const response = await fetchAPI<RawTopicPreview, typeof topicsListQuery>({
+ api: getAPIUrl(),
+ query: topicsListQuery,
+ variables: props,
+ });
+
+ return response.topics;
+};
+
+/**
+ * Convert raw data to a Topic object.
+ *
+ * @param {RawTopic} data - The page raw data.
+ * @returns {Topic} The page data.
+ */
+export const getTopicFromRawData = (data: RawTopic): Topic => {
+ const {
+ acfTopics,
+ contentParts,
+ databaseId,
+ date,
+ featuredImage,
+ info,
+ modified,
+ slug,
+ title,
+ seo,
+ } = data;
+
+ /**
+ * Retrieve an array of related topics.
+ *
+ * @param posts - The topic posts.
+ * @returns {PageLink[]} An array of topics links.
+ */
+ const getRelatedThematics = (posts: RawArticle[]): PageLink[] => {
+ const thematics: PageLink[] = [];
+
+ posts.forEach((post) => {
+ if (post.acfPosts.postsInThematic) {
+ post.acfPosts.postsInThematic.forEach((thematic) =>
+ thematics.push(getPageLinkFromRawData(thematic, 'thematic'))
+ );
+ }
+ });
+
+ const thematicsIds = thematics.map((thematic) => thematic.id);
+ const uniqueThematics = thematics.filter(
+ ({ id }, index) => !thematicsIds.includes(id, index + 1)
+ );
+ const sortThematicByName = (a: PageLink, b: PageLink) => {
+ var nameA = a.name.toUpperCase(); // ignore upper and lowercase
+ var nameB = b.name.toUpperCase(); // ignore upper and lowercase
+ if (nameA < nameB) {
+ return -1;
+ }
+ if (nameA > nameB) {
+ return 1;
+ }
+
+ // names must be equal
+ return 0;
+ };
+
+ return uniqueThematics.sort(sortThematicByName);
+ };
+
+ return {
+ content: contentParts.afterMore,
+ id: databaseId,
+ intro: contentParts.beforeMore,
+ meta: {
+ articles: acfTopics.postsInTopic.map((post) =>
+ getArticleFromRawData(post)
+ ),
+ cover: featuredImage?.node
+ ? getImageFromRawData(featuredImage.node)
+ : undefined,
+ dates: { publication: date, update: modified },
+ website: acfTopics.officialWebsite,
+ seo: {
+ description: seo?.metaDesc || '',
+ title: seo?.title || '',
+ },
+ thematics: getRelatedThematics(acfTopics.postsInTopic),
+ wordsCount: info.wordsCount,
+ },
+ slug,
+ title,
+ };
+};
+
+/**
+ * Retrieve a Topic object by slug.
+ *
+ * @param {string} slug - The topic slug.
+ * @returns {Promise<Article>} The requested topic.
+ */
+export const getTopicBySlug = async (slug: string): Promise<Topic> => {
+ const response = await fetchAPI<RawTopic, typeof topicBySlugQuery>({
+ api: getAPIUrl(),
+ query: topicBySlugQuery,
+ variables: { slug },
+ });
+
+ return getTopicFromRawData(response.topic);
+};
+
+/**
+ * Retrieve all the topics slugs.
+ *
+ * @returns {Promise<string[]>} - An array of topics slugs.
+ */
+export const getAllTopicsSlugs = async (): Promise<string[]> => {
+ const totalTopics = await getTotalTopics();
+ const response = await fetchAPI<Slug, typeof topicsSlugQuery>({
+ api: getAPIUrl(),
+ query: topicsSlugQuery,
+ variables: { first: totalTopics },
+ });
+
+ return response.topics.edges.map((edge) => edge.node.slug);
+};
diff --git a/src/services/local-storage/index.ts b/src/services/local-storage/index.ts
index 8ed9ebf..65235a7 100644
--- a/src/services/local-storage/index.ts
+++ b/src/services/local-storage/index.ts
@@ -1,15 +1,15 @@
export const LocalStorage = {
- get(key: string): string | null | undefined {
+ get<T>(key: string): T | undefined {
try {
const serialItem = localStorage.getItem(key);
if (!serialItem) return undefined;
- return JSON.parse(serialItem);
+ return JSON.parse(serialItem) as T;
} catch (e) {
console.log(e);
return undefined;
}
},
- set(key: string, value: string) {
+ set<T>(key: string, value: T) {
try {
const serialItem = JSON.stringify(value);
localStorage.setItem(key, serialItem);
diff --git a/src/styles/abstracts/_placeholders.scss b/src/styles/abstracts/_placeholders.scss
index d1c0a7a..18b1c03 100644
--- a/src/styles/abstracts/_placeholders.scss
+++ b/src/styles/abstracts/_placeholders.scss
@@ -1,3 +1,4 @@
@forward "./placeholders/animations";
@forward "./placeholders/clearfix";
+@forward "./placeholders/layout";
@forward "./placeholders/list";
diff --git a/src/styles/abstracts/placeholders/_layout.scss b/src/styles/abstracts/placeholders/_layout.scss
new file mode 100644
index 0000000..1a28acb
--- /dev/null
+++ b/src/styles/abstracts/placeholders/_layout.scss
@@ -0,0 +1,25 @@
+@use "@styles/abstracts/mixins" as mix;
+
+%grid {
+ display: grid;
+ align-items: center;
+ grid-template-columns:
+ minmax(0, 1fr) min(calc(100vw - calc(var(--spacing-md) * 2)), 80ch)
+ var(--column-3, minmax(0, 1fr));
+ column-gap: var(--grid-gap, var(--spacing-md));
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ grid-template-columns:
+ minmax(0, 1fr) clamp(60ch, 60vw, 80ch)
+ var(--column-3, minmax(0, 3fr));
+ column-gap: var(--grid-gap, var(--spacing-xl));
+ }
+
+ @include mix.dimensions("lg") {
+ grid-template-columns:
+ minmax(0, 1fr) clamp(47ch, 47vw, 80ch)
+ var(--column-3, minmax(0, 1fr));
+ }
+ }
+}
diff --git a/src/styles/base/_base.scss b/src/styles/base/_base.scss
index 25ef393..1ec5494 100644
--- a/src/styles/base/_base.scss
+++ b/src/styles/base/_base.scss
@@ -70,14 +70,6 @@ body {
flex-flow: column nowrap;
min-height: 100vh;
background: var(--color-bg);
- border-top: max(0.4vw, fun.convert-px(6)) solid;
- border-bottom: max(0.4vw, fun.convert-px(6)) solid;
- border-image: radial-gradient(
- ellipse at center,
- var(--color-primary-lighter) 20%,
- var(--color-primary) 100%
- )
- 1;
color: var(--color-fg);
font-family: var(--font-family-primary);
font-size: var(--font-size-md);
@@ -89,4 +81,12 @@ body {
display: flex;
flex-flow: column nowrap;
height: 100%;
+ border-top: max(0.4vw, fun.convert-px(6)) solid;
+ border-bottom: max(0.4vw, fun.convert-px(6)) solid;
+ border-image: radial-gradient(
+ ellipse at center,
+ var(--color-primary-lighter) 20%,
+ var(--color-primary) 100%
+ )
+ 1;
}
diff --git a/src/styles/base/_fonts.scss b/src/styles/base/_fonts.scss
index 88850bb..c8695d4 100644
--- a/src/styles/base/_fonts.scss
+++ b/src/styles/base/_fonts.scss
@@ -139,32 +139,32 @@
--font-family-mono: #{var.$font-family_mono};
--font-size-sm: clamp(
#{math.div(var.font-size("sm"), 1.2)},
- 1ex + 1vw,
+ 2ex + 1vmin,
#{var.font-size("sm")}
);
--font-size-md: clamp(
#{var.font-size("sm")},
- 1ex + 2vw,
+ 2ex + 2vmin,
#{var.font-size("md")}
);
--font-size-lg: clamp(
#{var.font-size("md")},
- 1ex + 3vw,
+ 2ex + 3vmin,
#{var.font-size("lg")}
);
--font-size-xl: clamp(
#{var.font-size("lg")},
- 1ex + 4vw,
+ 2ex + 4vmin,
#{var.font-size("xl")}
);
--font-size-2xl: clamp(
#{var.font-size("xl")},
- 1ex + 5vw,
+ 2ex + 5vmin,
#{var.font-size("2xl")}
);
--font-size-3xl: clamp(
#{var.font-size("2xl")},
- 1ex + 6vw,
+ 2ex + 6vmin,
#{var.font-size("3xl")}
);
--line-height: #{var.$line-height};
diff --git a/src/styles/base/_helpers.scss b/src/styles/base/_helpers.scss
index d28811c..3879643 100644
--- a/src/styles/base/_helpers.scss
+++ b/src/styles/base/_helpers.scss
@@ -28,17 +28,11 @@
display: block;
width: auto;
height: auto;
- padding: var(--spacing-xs) var(--spacing-sm);
left: var(--spacing-2xs);
top: var(--spacing-xs);
z-index: 100000;
- background: var(--color-primary);
- box-shadow: fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
- var(--color-shadow-dark);
clip: auto !important;
color: var(--color-fg-inverted);
- font-size: var(--font-size-md);
- font-weight: 600;
}
}
diff --git a/src/styles/base/_spacings.scss b/src/styles/base/_spacings.scss
index 08c3c3f..7c8b210 100644
--- a/src/styles/base/_spacings.scss
+++ b/src/styles/base/_spacings.scss
@@ -24,13 +24,5 @@
--spacing-xl: clamp(#{var.spacing("lg")}, 1ex + 4vw, #{var.spacing("xl")});
--spacing-2xl: clamp(#{var.spacing("xl")}, 1ex + 5vw, #{var.spacing("2xl")});
--spacing-3xl: clamp(#{var.spacing("2xl")}, 1ex + 6vw, #{var.spacing("3xl")});
- --toolbar-size: #{fun.convert-px(65)};
-}
-
-@include mix.media("screen") {
- @include mix.dimensions("sm") {
- :root {
- --toolbar-size: 0px;
- }
- }
+ --toolbar-size: #{fun.convert-px(80)};
}
diff --git a/src/styles/base/_typography.scss b/src/styles/base/_typography.scss
index f1cb38a..2c3c8cc 100644
--- a/src/styles/base/_typography.scss
+++ b/src/styles/base/_typography.scss
@@ -1,5 +1,4 @@
@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/variables" as var;
h1 {
font-size: var(--font-size-3xl);
@@ -116,34 +115,28 @@ dl {
ul,
ol,
dl {
- margin: var(--spacing-md) 0;
+ margin: var(--spacing-sm) 0;
& & {
margin: var(--spacing-2xs) 0 0;
}
-}
-
-dt {
- flex: 0 0 max-content;
- font-weight: 600;
-}
-dd {
- flex: 0 0 auto;
- margin: 0;
+ ::marker {
+ color: var(--color-primary-dark);
+ }
}
a {
background: linear-gradient(to top, var(--color-primary) 50%, transparent 50%)
- 0 0 / 100% 200% no-repeat;
+ 0 0 / 100% 201% no-repeat;
color: var(--color-primary);
- text-decoration-thickness: 13%;
+ text-decoration-thickness: 0.15em;
text-underline-offset: 20%;
transition: all 0.3s linear 0s, text-decoration 0.18s ease-in-out 0s;
&:hover {
color: var(--color-primary-light);
- text-decoration-thickness: 23%;
+ text-decoration-thickness: 0.25em;
}
&:focus {
@@ -156,39 +149,6 @@ a {
color: var(--color-primary-dark);
text-decoration-thickness: 18%;
}
-
- &.external {
- &::after {
- display: inline-block;
- content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
- }
-
- &:focus:not(:active)::after {
- content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
- }
- }
-
- &[hreflang] {
- &::after {
- display: inline-block;
- content: "\0000a0["attr(hreflang) "]";
- font-size: var(--font-size-sm);
- }
-
- &.external {
- &::after {
- content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
- '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
- ));
- }
-
- &:focus:not(:active)::after {
- content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
- '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
- ));
- }
- }
- }
}
button,
@@ -233,13 +193,3 @@ pre {
word-break: normal;
word-wrap: normal;
}
-
-figure {
- margin: var(--spacing-md) 0;
-}
-
-figcaption {
- margin-top: var(--spacing-xs);
- font-size: var(--font-size-sm);
- text-align: center;
-}
diff --git a/src/styles/components/_wp-blocks.scss b/src/styles/components/_wp-blocks.scss
deleted file mode 100644
index efd6db5..0000000
--- a/src/styles/components/_wp-blocks.scss
+++ /dev/null
@@ -1,166 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-@use "@styles/abstracts/placeholders";
-
-.wp-block-quote {
- margin: var(--spacing-sm) 0;
- padding: var(--spacing-sm);
- position: relative;
- border: fun.convert-px(1) solid var(--color-primary-lighter);
- border-left: fun.convert-px(5) solid var(--color-primary-lighter);
- box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
- fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
- var(--color-shadow-light);
- font-style: italic;
-
- > *:last-child {
- margin: 0;
- }
-
- cite {
- font-size: var(--font-size-sm);
- font-style: normal;
- font-weight: 600;
- }
-}
-
-.wp-block-code,
-.wp-block-preformatted {
- margin: 0 auto var(--spacing-md);
- padding: var(--spacing-xs) var(--spacing-sm);
- background: var(--color-bg-secondary);
- border: fun.convert-px(1) solid var(--color-border-light);
- color: var(--color-fg);
-}
-
-.wp-block-columns {
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- gap: var(--spacing-md);
- margin: var(--spacing-md) 0;
-
- @include mix.media("screen") {
- @include mix.dimensions("sm") {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- }
-
- &.are-vertically-aligned-center {
- align-items: center;
- }
-}
-
-.wp-block-column {
- > *:first-child {
- margin-top: 0;
- }
-
- > *:last-child {
- margin-bottom: 0;
- }
-}
-
-.wp-block-gallery {
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- gap: var(--spacing-sm);
-
- .blocks-gallery-grid {
- @extend %reset-list;
-
- grid-column: 1 / -1;
- grid-row: 1 / -1;
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- gap: var(--spacing-sm);
- }
-
- .blocks-gallery-item {
- figure {
- margin: 0;
- }
-
- a {
- display: block;
- box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
- fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
- var(--color-shadow-light);
-
- &:hover,
- &:focus {
- transform: scale(1.05);
- box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
- fun.convert-px(3) fun.convert-px(3) fun.convert-px(2) 0
- var(--color-shadow-light),
- fun.convert-px(5) fun.convert-px(5) fun.convert-px(8) 0
- var(--color-shadow-light);
- }
-
- &:focus {
- outline: solid var(--color-primary-light);
- }
-
- &:active {
- transform: scale(0.95);
- box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
- fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
- var(--color-shadow-light),
- 0 0 0 0 var(--color-shadow-light);
- outline: none;
- }
- }
- }
-
- &.aligncenter {
- .blocks-gallery-grid {
- align-items: center;
- }
- }
-
- @for $i from 0 to 6 {
- &.columns-#{$i} {
- @include mix.media("screen") {
- @include mix.dimensions("xs") {
- grid-template-columns: repeat(2, minmax(0, 1fr));
-
- .blocks-gallery-grid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- }
-
- @include mix.dimensions("sm") {
- grid-template-columns: repeat(#{$i}, minmax(0, 1fr));
-
- .blocks-gallery-grid {
- grid-template-columns: repeat(3, minmax(0, 1fr));
- }
- }
- }
- }
- }
-}
-
-.wp-block-image {
- img {
- display: block;
- margin: auto;
- box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
- fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
- var(--color-shadow-light);
- text-align: center;
- }
-}
-
-.wp-block-video {
- box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
- fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
- var(--color-shadow-light);
-}
diff --git a/src/styles/globals.scss b/src/styles/globals.scss
index f9a1281..8ece909 100644
--- a/src/styles/globals.scss
+++ b/src/styles/globals.scss
@@ -6,7 +6,6 @@
* Import each files separately to define vendors styles order.
*/
@use "modern-normalize";
-@use "vendors/prism";
/**
* 2.0. Base
@@ -22,14 +21,7 @@
@use "base/typography";
/**
- * 3.0. Components
- *
- * Define styles for external components (like WordPress blocks).
- */
-@use "components/wp-blocks";
-
-/**
- * 4.0. Themes
+ * 3.0. Themes
*
* Define themes specific styles.
*/
diff --git a/src/styles/pages/Home.module.scss b/src/styles/pages/Home.module.scss
deleted file mode 100644
index 8225a57..0000000
--- a/src/styles/pages/Home.module.scss
+++ /dev/null
@@ -1,49 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/placeholders";
-
-.links-list {
- @extend %flex-list;
-
- gap: var(--spacing-md);
- margin: 0 0 var(--spacing-md);
-}
-
-.icon--feed {
- width: fun.convert-px(20);
-}
-
-:global {
- [data-theme="dark"] {
- :local {
- .icon--feed {
- filter: brightness(0.8) contrast(1.1);
- }
- }
- }
-}
-
-.section {
- --icon-size: #{fun.convert-px(20)};
-
- composes: grid from "@styles/layout/_grid.scss";
- padding: var(--spacing-md) 0;
- background: var(--color-bg-secondary);
-
- &:not(:last-child) {
- border-bottom: fun.convert-px(1) solid var(--color-border);
- }
-
- &:nth-child(2n) {
- background: var(--color-bg);
- }
-
- > * {
- grid-column: 2;
- }
-
- :global {
- .wp-block-columns {
- margin: 0 0 var(--spacing-md);
- }
- }
-}
diff --git a/src/styles/pages/Projects.module.scss b/src/styles/pages/Projects.module.scss
deleted file mode 100644
index 3fd74cb..0000000
--- a/src/styles/pages/Projects.module.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-.article {
- composes: grid from "@styles/layout/_grid.scss";
- align-items: start;
-
- > header {
- grid-column: 1 / -1;
- }
-}
-
-.body {
- grid-column: 1 / -1;
- margin-bottom: var(--spacing-xl);
-}
diff --git a/src/styles/pages/article.module.scss b/src/styles/pages/article.module.scss
new file mode 100644
index 0000000..a5299fe
--- /dev/null
+++ b/src/styles/pages/article.module.scss
@@ -0,0 +1,37 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/variables" as var;
+@use "partials/article-links";
+@use "partials/article-lists";
+@use "partials/article-media";
+@use "partials/article-prism";
+@use "partials/article-wp-blocks";
+
+.btn {
+ margin-right: var(--spacing-2xs);
+ padding: var(--spacing-2xs) var(--spacing-xs);
+
+ figure {
+ max-width: fun.convert-px(22);
+ margin-right: var(--spacing-2xs);
+ }
+}
+
+.body {
+ :global {
+ @include article-links.styles;
+ @include article-lists.styles;
+ @include article-media.styles;
+ @include article-prism.styles;
+ @include article-wp-blocks.styles;
+ }
+}
+
+.widget {
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ width: min-content;
+ gap: var(--spacing-2xs);
+ }
+ }
+}
diff --git a/src/styles/pages/contact.module.scss b/src/styles/pages/contact.module.scss
new file mode 100644
index 0000000..65fb0d6
--- /dev/null
+++ b/src/styles/pages/contact.module.scss
@@ -0,0 +1,3 @@
+.notice {
+ margin-top: var(--spacing-md);
+}
diff --git a/src/styles/pages/cv.module.scss b/src/styles/pages/cv.module.scss
new file mode 100644
index 0000000..615c50d
--- /dev/null
+++ b/src/styles/pages/cv.module.scss
@@ -0,0 +1,3 @@
+.image {
+ max-width: min(100%, 25ch);
+}
diff --git a/src/styles/pages/home.module.scss b/src/styles/pages/home.module.scss
new file mode 100644
index 0000000..873a5a9
--- /dev/null
+++ b/src/styles/pages/home.module.scss
@@ -0,0 +1,36 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.section {
+ --card-width: 25ch;
+
+ &:last-of-type {
+ border-bottom: none;
+ }
+}
+
+.columns {
+ margin: 0 0 var(--spacing-sm);
+}
+
+.list {
+ margin: 0 0 var(--spacing-sm);
+
+ &--cards {
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ margin: 0 calc(var(--spacing-sm) * -1) var(--spacing-sm);
+ }
+ }
+ }
+}
+
+.icon {
+ --icon-size: #{fun.convert-px(20)};
+
+ margin-right: var(--spacing-2xs);
+
+ &--feed {
+ width: var(--icon-size);
+ }
+}
diff --git a/src/styles/pages/partials/_article-headings.scss b/src/styles/pages/partials/_article-headings.scss
new file mode 100644
index 0000000..c0c3519
--- /dev/null
+++ b/src/styles/pages/partials/_article-headings.scss
@@ -0,0 +1,57 @@
+@use "@styles/abstracts/functions" as fun;
+
+@mixin styles {
+ h1 {
+ font-size: var(--font-size-3xl);
+ font-weight: 500;
+ }
+
+ h2 {
+ padding-bottom: fun.convert-px(3);
+ background: linear-gradient(
+ to top,
+ var(--color-primary-dark) 0.3rem,
+ transparent 0.3rem
+ )
+ 0 0 / 3rem 100% no-repeat;
+ font-size: var(--font-size-2xl);
+ font-weight: 500;
+ text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
+ }
+
+ h3 {
+ font-size: var(--font-size-xl);
+ font-weight: 500;
+ }
+
+ h4 {
+ font-size: var(--font-size-lg);
+ font-weight: 500;
+ }
+
+ h5 {
+ font-size: var(--font-size-md);
+ font-weight: 600;
+ }
+
+ h6 {
+ font-size: var(--font-size-md);
+ font-weight: 500;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--color-primary-dark);
+ font-family: var(--font-family-secondary);
+ letter-spacing: 0.01ex;
+ margin: 0 0 var(--spacing-sm);
+
+ & + & {
+ margin-top: var(--spacing-md);
+ }
+ }
+}
diff --git a/src/styles/pages/partials/_article-links.scss b/src/styles/pages/partials/_article-links.scss
new file mode 100644
index 0000000..543337a
--- /dev/null
+++ b/src/styles/pages/partials/_article-links.scss
@@ -0,0 +1,204 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/variables" as var;
+
+@mixin styles {
+ a {
+ &[hreflang] {
+ &::after {
+ display: inline-block;
+
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]";
+ font-size: var(--font-size-sm);
+ }
+ }
+ }
+
+ /* stylelint-disable no-descending-specificity */
+ a.download {
+ &::after {
+ display: inline-block;
+
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_white}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+ }
+ }
+
+ a.external {
+ &::after {
+ display: inline-block;
+
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+
+ a.external.download {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+
+ [data-theme="dark"] {
+ a.download {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'));
+ }
+ }
+ }
+
+ a.external {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+
+ a.external.download {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &[hreflang] {
+ &::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+
+ &:focus:not(:active)::after {
+ /* Prettier is removing spacing between content parts. */
+
+ /* prettier-ignore */
+ content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$dark-theme_black}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>')) "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$dark-theme_black}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$dark-theme_black}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+ }
+ }
+ }
+ }
+}
+/* stylelint-enable no-descending-specificity */
diff --git a/src/styles/pages/partials/_article-lists.scss b/src/styles/pages/partials/_article-lists.scss
new file mode 100644
index 0000000..c0084b0
--- /dev/null
+++ b/src/styles/pages/partials/_article-lists.scss
@@ -0,0 +1,65 @@
+@mixin styles {
+ ol {
+ padding: 0;
+ list-style-type: none;
+ counter-reset: li;
+
+ > li {
+ display: table;
+ counter-increment: li;
+
+ &::before {
+ content: counters(li, ".") ". ";
+ display: table-cell;
+ padding-right: var(--spacing-2xs);
+ color: var(--color-secondary);
+ }
+ }
+
+ li ol > li::before {
+ content: counters(li, ".") ". ";
+ }
+ }
+
+ ul,
+ ol {
+ li:not(:last-child) {
+ margin-bottom: var(--spacing-2xs);
+ }
+
+ ::marker {
+ color: var(--color-primary-dark);
+ }
+ }
+
+ ul {
+ padding-left: var(--spacing-sm);
+ }
+
+ dl {
+ display: flex;
+ flex-flow: row wrap;
+ gap: var(--spacing-2xs);
+ width: fit-content;
+ }
+
+ ul,
+ ol,
+ dl {
+ margin: var(--spacing-sm) 0;
+
+ & & {
+ margin: var(--spacing-2xs) 0 0;
+ }
+ }
+
+ dt {
+ color: var(--color-fg-light);
+ font-weight: 600;
+ }
+
+ dd {
+ margin: 0;
+ word-break: break-all;
+ }
+}
diff --git a/src/styles/pages/partials/_article-media.scss b/src/styles/pages/partials/_article-media.scss
new file mode 100644
index 0000000..0cd3654
--- /dev/null
+++ b/src/styles/pages/partials/_article-media.scss
@@ -0,0 +1,20 @@
+@use "@styles/abstracts/functions" as fun;
+
+@mixin styles {
+ figure {
+ display: flex;
+ flex-flow: column;
+ width: fit-content;
+ margin: 0 auto;
+ position: relative;
+ text-align: center;
+ }
+
+ figcaption {
+ margin: 0;
+ padding: fun.convert-px(4) var(--spacing-2xs);
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(1) solid var(--color-border-light);
+ font-weight: 500;
+ }
+}
diff --git a/src/styles/pages/partials/_article-prism.scss b/src/styles/pages/partials/_article-prism.scss
new file mode 100644
index 0000000..a714eb6
--- /dev/null
+++ b/src/styles/pages/partials/_article-prism.scss
@@ -0,0 +1,302 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+@mixin styles {
+ .code-toolbar {
+ --toolbar-height: #{fun.convert-px(100)};
+
+ position: relative;
+ margin-top: calc(var(--toolbar-height) + var(--spacing-sm));
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ --toolbar-height: #{fun.convert-px(60)};
+ }
+ }
+
+ .toolbar {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: center;
+ width: 100%;
+ height: var(--toolbar-height);
+ position: absolute;
+ top: calc(var(--toolbar-height) * -1);
+ left: 0;
+ right: 0;
+ background: var(--color-bg-tertiary);
+ border: fun.convert-px(1) solid var(--color-border);
+ }
+
+ .toolbar-item {
+ display: flex;
+ align-items: center;
+ margin: 0 var(--spacing-2xs);
+ }
+
+ .toolbar-item:nth-child(1) {
+ flex: 0 0 100%;
+ justify-content: center;
+ margin: 0 auto 0 0;
+ padding: 0 var(--spacing-sm);
+ background: var(--color-bg-code);
+ border-bottom: fun.convert-px(1) solid var(--color-border);
+ color: var(--color-primary-darker);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ flex: 0 0 auto;
+ justify-content: left;
+ border-bottom: none;
+ border-right: fun.convert-px(1) solid var(--color-border);
+ }
+ }
+ }
+ }
+
+ .copy-to-clipboard-button,
+ .prism-color-scheme-button {
+ display: block;
+ padding: fun.convert-px(3) var(--spacing-xs);
+ background: var(--color-bg);
+ border: 0.4ex solid var(--color-primary);
+ border-radius: fun.convert-px(30);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
+ var(--color-shadow),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
+ var(--color-shadow);
+ color: var(--color-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ transition: all 0.35s ease-in-out 0s;
+
+ &:hover,
+ &:focus {
+ transform: translateX(#{fun.convert-px(-2)})
+ translateY(#{fun.convert-px(-2)});
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow-light),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
+ var(--color-shadow-light),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
+ var(--color-shadow-light),
+ fun.convert-px(4) fun.convert-px(7) fun.convert-px(8) fun.convert-px(-3)
+ var(--color-shadow-light);
+ }
+
+ &:focus {
+ text-decoration: underline var(--color-primary) fun.convert-px(3);
+ }
+
+ &:active {
+ text-decoration: none;
+ transform: translateY(#{fun.convert-px(2)});
+ box-shadow: 0 0 0 0 var(--color-shadow);
+ }
+ }
+
+ pre[class*="language-"] {
+ --gutter-size-with-spacing: calc(var(--gutter-size) + var(--spacing-xs));
+
+ padding: 0;
+ position: relative;
+ overflow: auto;
+ border: fun.convert-px(1) solid var(--color-border-light);
+ hyphens: none;
+ tab-size: 4;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ word-wrap: normal;
+
+ &.command-line {
+ --gutter-size: 19ch;
+ padding-left: var(--gutter-size-with-spacing);
+ }
+
+ &.line-numbers {
+ --gutter-size: 6ch;
+
+ counter-reset: lineNumber;
+ padding-left: var(--gutter-size-with-spacing);
+ }
+
+ code {
+ display: block;
+ padding: var(--spacing-xs) 0;
+ position: relative;
+ }
+
+ .line-numbers-rows,
+ .command-line-prompt {
+ display: block;
+ width: var(--gutter-size);
+ padding: var(--spacing-xs) 0;
+ position: absolute;
+ top: 0;
+ left: calc(var(--gutter-size-with-spacing) * -1);
+ background: var(--color-bg);
+ border-right: fun.convert-px(1) solid var(--color-border);
+ font-size: 100%;
+ letter-spacing: -1px;
+ text-align: right;
+ pointer-events: none;
+ user-select: none;
+
+ > span {
+ &::before {
+ display: block;
+ padding-right: var(--spacing-xs);
+ color: var(--color-fg-light);
+ }
+ }
+ }
+
+ .command-line-prompt {
+ > span {
+ &::before {
+ content: " ";
+ }
+
+ &[data-user]::before {
+ content: "[" attr(data-user) "@" attr(data-host) "] $";
+ }
+
+ &[data-user="root"]::before {
+ content: "[" attr(data-user) "@" attr(data-host) "] #";
+ }
+
+ &[data-prompt]::before {
+ content: attr(data-prompt);
+ }
+
+ &[data-continuation-prompt]::before {
+ content: attr(data-continuation-prompt);
+ }
+ }
+ }
+
+ .line-numbers-rows {
+ > span {
+ counter-increment: lineNumber;
+
+ &::before {
+ content: counter(lineNumber);
+ }
+ }
+ }
+
+ .token {
+ &.comment,
+ &.doc-comment {
+ color: var(--color-fg-light);
+ }
+
+ &.punctuation {
+ color: var(--color-fg);
+ }
+
+ &.attr-name,
+ &.hexcode,
+ &.inserted,
+ &.string {
+ color: var(--color-token-green);
+ }
+
+ &.class,
+ &.coord,
+ &.id,
+ &.function {
+ color: var(--color-token-purple);
+ }
+
+ &.builtin,
+ &.builtin.class-name,
+ &.property-access,
+ &.regex,
+ &.scope {
+ color: var(--color-token-magenta);
+ }
+
+ &.class-name,
+ &.constant,
+ &.global,
+ &.interpolation,
+ &.key,
+ &.package,
+ &.this,
+ &.title,
+ &.variable {
+ color: var(--color-token-blue);
+ }
+
+ &.combinator,
+ &.keyword,
+ &.operator,
+ &.pseudo-class,
+ &.pseudo-element,
+ &.rule,
+ &.selector,
+ &.unit {
+ color: var(--color-token-orange);
+ }
+
+ &.attr-value,
+ &.boolean,
+ &.number {
+ color: var(--color-token-yellow);
+ }
+
+ &.delimiter,
+ &.doctype,
+ &.parameter,
+ &.parent,
+ &.property,
+ &.shebang,
+ &.tag {
+ color: var(--color-token-cyan);
+ }
+
+ &.deleted {
+ color: var(--color-token-red);
+ }
+
+ &.punctuation.brace-hover,
+ &.punctuation.brace-selected {
+ background: var(--color-bg);
+ outline: solid fun.convert-px(1) var(--color-primary-light);
+ }
+ }
+
+ span.inline-color-wrapper {
+ background: url(fun.encode-svg(
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="gray" d="M0 0h2v2H0z"/><path fill="white" d="M0 0h1v1H0zM1 1h1v1H1z"/></svg>'
+ ));
+
+ /* Prevent glitches where 1px from the repeating pattern could be seen. */
+ background-position: center;
+ background-size: 110%;
+
+ display: inline-block;
+ height: 1.1ch;
+ width: 1.1ch;
+ margin: 0 0.5ch 0 0;
+ border: fun.convert-px(1) solid var(--color-bg);
+ outline: fun.convert-px(1) solid var(--color-border-dark);
+ overflow: hidden;
+ }
+
+ span.inline-color {
+ display: block;
+
+ /* To prevent visual glitches again */
+ height: 120%;
+ width: 120%;
+ }
+ }
+}
diff --git a/src/styles/pages/partials/_article-wp-blocks.scss b/src/styles/pages/partials/_article-wp-blocks.scss
new file mode 100644
index 0000000..86be062
--- /dev/null
+++ b/src/styles/pages/partials/_article-wp-blocks.scss
@@ -0,0 +1,177 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+@use "@styles/abstracts/placeholders";
+
+@mixin styles {
+ .wp-block-quote {
+ margin: var(--spacing-sm) 0;
+ padding: var(--spacing-sm);
+ position: relative;
+ border: fun.convert-px(1) solid var(--color-border-dark);
+ border-left: fun.convert-px(5) solid var(--color-border-dark);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+ var(--color-shadow),
+ fun.convert-px(3) fun.convert-px(3) fun.convert-px(4) 0
+ var(--color-shadow);
+ font-style: italic;
+
+ > *:last-child {
+ margin: 0;
+ }
+
+ cite {
+ color: var(--color-fg-light);
+ font-size: var(--font-size-sm);
+ font-style: normal;
+ font-weight: 600;
+ }
+ }
+
+ .wp-block-code,
+ .wp-block-preformatted {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ background: var(--color-bg-secondary);
+ border: fun.convert-px(1) solid var(--color-border-light);
+ color: var(--color-fg);
+ }
+
+ .wp-block-columns {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: var(--spacing-md);
+ margin: var(--spacing-md) 0;
+
+ @include mix.media("screen") {
+ @include mix.dimensions("sm") {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ }
+
+ &.are-vertically-aligned-center {
+ align-items: center;
+ }
+ }
+
+ .wp-block-column {
+ > *:first-child {
+ margin-top: 0;
+ }
+
+ > *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .wp-block-image,
+ .wp-block-video {
+ padding: fun.convert-px(4);
+ border: fun.convert-px(1) solid var(--color-border);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+ var(--color-shadow);
+ }
+
+ .wp-block-image {
+ display: flex;
+ flex-flow: column;
+ width: fit-content;
+ margin: 0 auto;
+ position: relative;
+ text-align: center;
+
+ img {
+ margin: auto;
+ }
+
+ figcaption {
+ margin-top: fun.convert-px(4);
+ font-size: var(--font-size-sm);
+ }
+ }
+
+ .wp-block-gallery {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: var(--spacing-sm);
+
+ .blocks-gallery-grid {
+ @extend %reset-list;
+
+ grid-column: 1 / -1;
+ grid-row: 1 / -1;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: var(--spacing-sm);
+ }
+
+ .blocks-gallery-item {
+ figure {
+ margin: 0;
+ }
+
+ a {
+ display: block;
+ box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
+ fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
+ var(--color-shadow-light),
+ fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
+ var(--color-shadow-light);
+
+ &:hover,
+ &:focus {
+ transform: scale(1.05);
+ box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
+ fun.convert-px(3) fun.convert-px(3) fun.convert-px(2) 0
+ var(--color-shadow-light),
+ fun.convert-px(5) fun.convert-px(5) fun.convert-px(8) 0
+ var(--color-shadow-light);
+ }
+
+ &:focus {
+ outline: solid var(--color-primary-light);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
+ fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
+ var(--color-shadow-light),
+ 0 0 0 0 var(--color-shadow-light);
+ outline: none;
+ }
+ }
+ }
+
+ &.aligncenter {
+ .blocks-gallery-grid {
+ align-items: center;
+ }
+ }
+
+ @for $i from 0 to 6 {
+ &.columns-#{$i} {
+ @include mix.media("screen") {
+ @include mix.dimensions("xs") {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+
+ .blocks-gallery-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ }
+
+ @include mix.dimensions("sm") {
+ grid-template-columns: repeat(#{$i}, minmax(0, 1fr));
+
+ .blocks-gallery-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+ }
+ }
+ }
+ }
+
+ .wp-block-image img {
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+}
diff --git a/src/styles/pages/project.module.scss b/src/styles/pages/project.module.scss
new file mode 100644
index 0000000..3b1b5cc
--- /dev/null
+++ b/src/styles/pages/project.module.scss
@@ -0,0 +1,10 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.widget {
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ width: min-content;
+ gap: var(--spacing-2xs);
+ }
+ }
+}
diff --git a/src/styles/pages/projects.module.scss b/src/styles/pages/projects.module.scss
new file mode 100644
index 0000000..fde1f31
--- /dev/null
+++ b/src/styles/pages/projects.module.scss
@@ -0,0 +1,11 @@
+@use "@styles/abstracts/mixins" as mix;
+
+.list {
+ margin-top: var(--spacing-sm);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("md") {
+ margin: var(--spacing-md) calc(var(--spacing-3xl) * -1) var(--spacing-md);
+ }
+ }
+}
diff --git a/src/styles/pages/topic.module.scss b/src/styles/pages/topic.module.scss
new file mode 100644
index 0000000..fd5f742
--- /dev/null
+++ b/src/styles/pages/topic.module.scss
@@ -0,0 +1,6 @@
+@use "@styles/abstracts/functions" as fun;
+
+.logo {
+ max-width: fun.convert-px(50);
+ margin: 0 var(--spacing-xs) 0 0;
+}
diff --git a/src/styles/vendors/_prism.scss b/src/styles/vendors/_prism.scss
deleted file mode 100644
index 7c05c9f..0000000
--- a/src/styles/vendors/_prism.scss
+++ /dev/null
@@ -1,297 +0,0 @@
-@use "@styles/abstracts/functions" as fun;
-@use "@styles/abstracts/mixins" as mix;
-
-/// Custom theme for Prism
-
-.code-toolbar {
- --gutter-size: clamp(#{fun.convert-px(75)}, 20vw, #{fun.convert-px(90)});
- --toolbar-height: #{fun.convert-px(90)};
-
- position: relative;
- margin-top: calc(var(--toolbar-height) + var(--spacing-md));
-
- @include mix.media("screen") {
- @include mix.dimensions("2xs") {
- --toolbar-height: #{fun.convert-px(60)};
- }
- }
-
- .toolbar {
- display: grid;
- grid-template-columns: max-content minmax(0, 1fr);
- justify-items: end;
- width: 100%;
- height: var(--toolbar-height);
- position: absolute;
- top: calc(var(--toolbar-height) * -1);
- left: 0;
- right: 0;
- background: var(--color-bg-tertiary);
- border: fun.convert-px(1) solid var(--color-border);
-
- @include mix.media("screen") {
- @include mix.dimensions("2xs") {
- display: flex;
- flex-flow: row wrap;
- }
- }
- }
-
- .toolbar-item {
- display: flex;
- align-items: center;
- }
-
- .toolbar-item:nth-child(1) {
- grid-column: 1;
- grid-row: 1 / 3;
- margin-right: auto;
- padding: 0 var(--spacing-sm);
- background: var(--color-bg-code);
- border-right: fun.convert-px(1) solid var(--color-border);
- color: var(--color-primary-darker);
- font-size: var(--font-size-sm);
- font-weight: 600;
- }
-
- .toolbar-item:nth-child(2) {
- grid-column: 2;
- grid-row: 1;
- margin: 0 var(--spacing-2xs);
- }
-
- .toolbar-item:nth-child(3) {
- grid-column: 2;
- grid-row: 2;
- margin: 0 var(--spacing-2xs);
- }
-}
-
-pre[class*="language-"] {
- max-height: max(30vw, fun.convert-px(300));
- margin: var(--spacing-md) 0;
- padding: 0;
- position: relative;
- background: var(--color-bg-secondary);
- color: var(--color-fg);
- border: fun.convert-px(1) solid var(--color-border);
-
- > code {
- display: block;
- padding: var(--spacing-xs) 0 var(--spacing-xs)
- calc(var(--gutter-size) + var(--spacing-xs));
- }
-
- .line-numbers-rows,
- .command-line-prompt {
- width: var(--gutter-size);
- min-height: 100%;
- padding: var(--spacing-xs) var(--spacing-2xs);
- position: absolute;
- top: 0;
- left: 0;
- pointer-events: none;
- user-select: none;
- background: var(--color-bg);
- border-right: fun.convert-px(1) solid var(--color-border);
- }
-
- .token {
- &.comment,
- &.doc-comment {
- color: var(--color-fg-light);
- }
-
- &.punctuation {
- color: var(--color-fg);
- }
-
- &.attr-name,
- &.hexcode,
- &.inserted,
- &.string {
- color: var(--color-token-green);
- }
-
- &.class,
- &.coord,
- &.id,
- &.function {
- color: var(--color-token-purple);
- }
-
- &.builtin,
- &.builtin.class-name,
- &.property-access,
- &.regex,
- &.scope {
- color: var(--color-token-magenta);
- }
-
- &.class-name,
- &.constant,
- &.global,
- &.interpolation,
- &.key,
- &.package,
- &.this,
- &.title,
- &.variable {
- color: var(--color-token-blue);
- }
-
- &.combinator,
- &.keyword,
- &.operator,
- &.pseudo-class,
- &.pseudo-element,
- &.rule,
- &.selector,
- &.unit {
- color: var(--color-token-orange);
- }
-
- &.attr-value,
- &.boolean,
- &.number {
- color: var(--color-token-yellow);
- }
-
- &.delimiter,
- &.doctype,
- &.parameter,
- &.parent,
- &.property,
- &.shebang,
- &.tag {
- color: var(--color-token-cyan);
- }
-
- &.deleted {
- color: var(--color-token-red);
- }
-
- &.punctuation.brace-hover,
- &.punctuation.brace-selected {
- background: var(--color-bg);
- outline: solid fun.convert-px(1) var(--color-primary-light);
- }
- }
-
- span.inline-color-wrapper {
- background: url(fun.encode-svg(
- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="gray" d="M0 0h2v2H0z"/><path fill="white" d="M0 0h1v1H0zM1 1h1v1H1z"/></svg>'
- ));
-
- /* Prevent glitches where 1px from the repeating pattern could be seen. */
- background-position: center;
- background-size: 110%;
-
- display: inline-block;
- height: 1.1ch;
- width: 1.1ch;
- margin: 0 0.5ch 0 0;
- border: fun.convert-px(1) solid var(--color-bg);
- outline: fun.convert-px(1) solid var(--color-border-dark);
- overflow: hidden;
- }
-
- span.inline-color {
- display: block;
-
- /* To prevent visual glitches again */
- height: 120%;
- width: 120%;
- }
-}
-
-pre.line-numbers {
- counter-reset: lineNumber;
-
- .line-numbers-rows {
- > span {
- counter-increment: lineNumber;
-
- &::before {
- display: block;
- padding: 0 var(--spacing-xs);
- content: counter(lineNumber);
- color: var(--color-primary-darker);
- text-align: right;
- line-height: var(--line-height);
- }
- }
- }
-}
-
-pre.command-line {
- --gutter-size: clamp(#{fun.convert-px(195)}, 48vw, #{fun.convert-px(235)});
-
- ~ .toolbar {
- --gutter-size: clamp(#{fun.convert-px(195)}, 48vw, #{fun.convert-px(235)});
- }
-
- .command-line-prompt {
- > span {
- &::before {
- display: block;
- content: "";
- }
-
- &[data-user]::before {
- content: "[" attr(data-user) "@" attr(data-host) "] $";
- }
-
- &[data-user="root"]::before {
- content: "[" attr(data-user) "@" attr(data-host) "] #";
- }
-
- &[data-prompt]::before {
- content: attr(data-prompt);
- }
- }
- }
-}
-
-.copy-to-clipboard-button,
-.prism-color-scheme-button {
- display: block;
- padding: fun.convert-px(3) var(--spacing-xs);
- background: var(--color-bg);
- border: 0.4ex solid var(--color-primary);
- border-radius: fun.convert-px(30);
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow);
- color: var(--color-primary);
- font-size: var(--font-size-sm);
- font-weight: 600;
- transition: all 0.35s ease-in-out 0s;
-
- &:hover,
- &:focus {
- transform: translateX(#{fun.convert-px(-2)})
- translateY(#{fun.convert-px(-2)});
- box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
- var(--color-shadow-light),
- fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
- var(--color-shadow-light),
- fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
- var(--color-shadow-light),
- fun.convert-px(4) fun.convert-px(7) fun.convert-px(8) fun.convert-px(-3)
- var(--color-shadow-light);
- }
-
- &:focus {
- text-decoration: underline var(--color-primary) fun.convert-px(3);
- }
-
- &:active {
- text-decoration: none;
- transform: translateY(#{fun.convert-px(2)});
- box-shadow: 0 0 0 0 var(--color-shadow);
- }
-}
diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts
index 4243762..7bf1541 100644
--- a/src/ts/types/app.ts
+++ b/src/ts/types/app.ts
@@ -1,160 +1,122 @@
import { NextPage } from 'next';
import { AppProps } from 'next/app';
-import { ImageProps } from 'next/image';
import { ReactElement, ReactNode } from 'react';
-import { PostBy, TotalArticles } from './articles';
-import { AllPostsSlug, LastPostCursor, RawPostsList } from './blog';
-import { CommentData, CommentsByPostId, CreateComment } from './comments';
-import { ContactData, SendEmail } from './contact';
-import {
- AllTopics,
- AllTopicsSlug,
- AllThematics,
- AllThematicsSlug,
- TopicBy,
- ThematicBy,
-} from './taxonomies';
-
-//==============================================================================
-// Next
-//==============================================================================
-
-export type NextPageWithLayout<P = {}> = NextPage<P> & {
- getLayout?: (page: ReactElement) => ReactNode;
+
+export type NextPageWithLayoutOptions = {
+ withExtraPadding?: boolean;
+ isHome?: boolean;
+ useGrid?: boolean;
+};
+
+export type NextPageWithLayout<T = {}> = NextPage<T> & {
+ getLayout?: (
+ page: ReactElement,
+ options: NextPageWithLayoutOptions
+ ) => ReactNode;
};
export type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
-//==============================================================================
-// API
-//==============================================================================
-
-export type VariablesType<T> = T extends PostBy | TopicBy | ThematicBy
- ? Slug
- : T extends RawPostsList
- ? CursorPagination
- : T extends CommentsByPostId
- ? { id: number }
- : T extends CreateComment
- ? CommentData
- : T extends LastPostCursor
- ? { first: number }
- : T extends SendEmail
- ? ContactData
- : null;
-
-export type RequestType =
- | AllPostsSlug
- | AllTopics
- | AllTopicsSlug
- | AllThematics
- | AllThematicsSlug
- | CommentsByPostId
- | CreateComment
- | LastPostCursor
- | PostBy
- | RawPostsList
- | SendEmail
- | ThematicBy
- | TopicBy
- | TotalArticles;
-
-//==============================================================================
-// Globals
-//==============================================================================
-
-export type ButtonKind = 'primary' | 'secondary' | 'tertiary';
-
-export type ButtonPosition = 'left' | 'right' | 'center';
-
-export type ContentInfo = {
- readingTime: number;
- wordsCount: number;
+export type ContentKind =
+ | 'article'
+ | 'comment'
+ | 'page'
+ | 'project'
+ | 'thematic'
+ | 'topic';
+
+export type Author<T extends ContentKind> = {
+ avatar?: Image;
+ description?: T extends 'comment' ? never : string;
+ name: string;
+ website?: string;
};
-export type ContentParts = {
- afterMore: string;
- beforeMore: string;
+export type CommentMeta = {
+ author: Author<'comment'>;
+ date: string;
};
-export type CursorPagination = {
- first: number;
- after: string;
+export type Comment = {
+ approved: boolean;
+ content: string;
+ id: number;
+ meta: CommentMeta;
+ parentId?: number;
+ replies: Comment[];
};
export type Dates = {
publication: string;
- update: string;
-};
-
-export type Heading = {
- depth: number;
- id: string;
- children: Heading[];
- title: string;
+ update?: string;
};
-export type Meta = {
- title: string;
- publishedOn: string;
- updatedOn: string;
+export type Image = {
+ alt: string;
+ height: number;
+ src: string;
+ title?: string;
+ width: number;
};
-export type MetaKind = 'article' | 'list';
-
-export type NoticeType = 'error' | 'info' | 'success' | 'warning';
-
-export type PageInfo = {
- endCursor: string;
- hasNextPage: boolean;
- total: number;
+export type Repos = {
+ github?: string;
+ gitlab?: string;
};
-export type ParamsIds = {
- params: { id: string };
+export type SEO = {
+ description: string;
+ title: string;
};
-export type ParamsSlug = {
- params: { slug: string };
+export type PageKind = Exclude<ContentKind, 'comment'>;
+
+export type Meta<T extends PageKind> = {
+ articles?: T extends 'thematic' | 'topic' ? Article[] : never;
+ author?: T extends 'article' | 'page' ? Author<T> : never;
+ commentsCount?: T extends 'article' ? number : never;
+ cover?: Image;
+ dates: Dates;
+ license?: T extends 'project' ? string : never;
+ repos?: T extends 'project' ? Repos : never;
+ seo: SEO;
+ tagline?: T extends 'project' ? string : never;
+ technologies?: T extends 'project' ? string[] : never;
+ thematics?: T extends 'article' | 'topic' ? PageLink[] : never;
+ topics?: T extends 'article' | 'thematic' ? PageLink[] : never;
+ website?: T extends 'topic' ? string : never;
+ wordsCount: number;
};
-export type Project = {
- cover?: string;
- id: string;
+export type Page<T extends PageKind> = {
+ content: string;
+ id: number | string;
intro: string;
- meta: ProjectMeta;
+ meta: Meta<T>;
slug: string;
- tagline?: string;
title: string;
- seo: {
- title: string;
- description: string;
- };
-};
-
-export type ProjectMeta = Omit<Meta, 'title'> & {
- hasCover: boolean;
- license: string;
- repos?: {
- github?: string;
- gitlab?: string;
- };
- technologies?: string[];
};
-export type ProjectProps = {
- project: Project;
+export type PageLink = {
+ id: number;
+ logo?: Image;
+ name: string;
+ url: string;
};
-export type ResponsiveImageProps = ImageProps & {
- caption?: string;
- linkTarget?: string;
+export type Article = Page<'article'>;
+export type ArticleCard = Pick<Article, 'id' | 'slug' | 'title'> &
+ Pick<Meta<'article'>, 'cover' | 'dates'>;
+export type Project = Page<'project'>;
+export type ProjectPreview = Omit<Page<'project'>, 'content'>;
+export type ProjectCard = Pick<Page<'project'>, 'id' | 'slug' | 'title'> & {
+ meta: Pick<Meta<'project'>, 'cover' | 'dates' | 'tagline' | 'technologies'>;
};
+export type Thematic = Page<'thematic'>;
+export type Topic = Page<'topic'>;
export type Slug = {
slug: string;
};
-
-export type TitleLevel = 2 | 3 | 4 | 5 | 6;
diff --git a/src/ts/types/articles.ts b/src/ts/types/articles.ts
deleted file mode 100644
index 64d2860..0000000
--- a/src/ts/types/articles.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-import { ContentInfo, ContentParts, Dates } from './app';
-import { Comment } from './comments';
-import { Cover, RawCover } from './cover';
-import { SEO } from './seo';
-import { RawTopicPreview, TopicPreview, ThematicPreview } from './taxonomies';
-
-export type ArticleAuthor = {
- firstName: string;
- lastName: string;
- name: string;
-};
-
-export type RawACFPosts = {
- postsInTopic: RawTopicPreview[] | null;
- postsInThematic: ThematicPreview[] | null;
-};
-
-export type ACFPosts = {
- postsInTopic: TopicPreview[] | null;
- postsInThematic: ThematicPreview[] | null;
-};
-
-export type ArticleMeta = {
- author?: ArticleAuthor;
- commentCount?: number;
- dates?: Dates;
- readingTime?: number;
- results?: number;
- topics?: TopicPreview[];
- thematics?: ThematicPreview[];
- website?: string;
- wordsCount?: number;
-};
-
-export type Article = {
- author: ArticleAuthor;
- commentCount: number | null;
- content: string;
- databaseId: number;
- dates: Dates;
- featuredImage: Cover;
- id: string;
- info: ContentInfo;
- intro: string;
- seo: SEO;
- topics: TopicPreview[] | [];
- thematics: ThematicPreview[] | [];
- title: string;
-};
-
-export type RawArticle = Pick<
- Article,
- 'commentCount' | 'databaseId' | 'id' | 'info' | 'seo' | 'title'
-> & {
- acfPosts: RawACFPosts;
- author: { node: ArticleAuthor };
- contentParts: ContentParts;
- date: string;
- featuredImage: RawCover;
- modified: string;
-};
-
-export type ArticlePreview = Pick<
- Article,
- | 'commentCount'
- | 'dates'
- | 'id'
- | 'info'
- | 'intro'
- | 'topics'
- | 'thematics'
- | 'title'
-> & { featuredImage: Cover; slug: string };
-
-export type RawArticlePreview = Pick<
- Article,
- 'commentCount' | 'id' | 'info' | 'title'
-> & {
- acfPosts: ACFPosts;
- contentParts: Pick<ContentParts, 'beforeMore'>;
- date: string;
- featuredImage: RawCover;
- modified: string;
- slug: string;
-};
-
-export type PostBy = {
- post: RawArticle;
-};
-
-export type ArticleProps = {
- comments: Comment[];
- post: Article;
-};
-
-export type TotalArticles = {
- posts: {
- pageInfo: {
- total: number;
- };
- };
-};
diff --git a/src/ts/types/blog.ts b/src/ts/types/blog.ts
deleted file mode 100644
index 05bdd1f..0000000
--- a/src/ts/types/blog.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { PageInfo, Slug } from './app';
-import { ArticlePreview, RawArticlePreview } from './articles';
-import { ThematicPreview, TopicPreview } from './taxonomies';
-
-export type PostsList = {
- posts: ArticlePreview[];
- pageInfo: PageInfo;
-};
-
-export type PostsListEdges = {
- cursor: string;
- node: RawArticlePreview;
-};
-
-export type RawPostsList = {
- posts: {
- edges: PostsListEdges[];
- pageInfo: PageInfo;
- };
-};
-
-export type LastPostCursor = {
- posts: {
- pageInfo: {
- endCursor: string;
- };
- };
-};
-
-export type AllPostsSlug = {
- posts: {
- nodes: Slug[];
- };
-};
-
-export type BlogPageProps = {
- allThematics: ThematicPreview[];
- allTopics: TopicPreview[];
- posts: PostsList;
- totalPosts: number;
-};
diff --git a/src/ts/types/comments.ts b/src/ts/types/comments.ts
deleted file mode 100644
index aa3fac3..0000000
--- a/src/ts/types/comments.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-//==============================================================================
-// Comments query
-//==============================================================================
-
-export type CommentAuthor = {
- name: string;
- gravatarUrl: string;
- url: string;
-};
-
-export type RawCommentAuthor = {
- node: CommentAuthor;
-};
-
-export type Comment = {
- approved: '';
- author: CommentAuthor;
- databaseId: number;
- content: string;
- date: string;
- parentDatabaseId: number;
- replies: Comment[];
-};
-
-export type RawComment = Omit<Comment, 'author' | 'replies'> & {
- author: RawCommentAuthor;
-};
-
-export type CommentsNode = {
- nodes: RawComment[];
-};
-
-export type CommentsByPostId = {
- comments: CommentsNode;
-};
-
-//==============================================================================
-// Comment mutations
-//==============================================================================
-
-export type CommentData = {
- author: string;
- authorEmail: string;
- authorUrl: string;
- content: string;
- parent: number;
- commentOn: number;
- mutationId: string;
-};
-
-export type CreatedComment = {
- clientMutationId: string;
- success: boolean;
- comment: null | {
- approved: boolean;
- };
-};
-
-export type CreateComment = {
- createComment: CreatedComment;
-};
diff --git a/src/ts/types/contact.ts b/src/ts/types/contact.ts
deleted file mode 100644
index ef6847a..0000000
--- a/src/ts/types/contact.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export type ContactData = {
- body: string;
- mutationId: string;
- replyTo: string;
- subject: string;
-};
-
-export type SentEmail = {
- clientMutationId: string;
- message: string;
- origin: string;
- replyTo: string;
- sent: boolean;
- to: string;
-};
-
-export type SendEmail = {
- sendEmail: SentEmail;
-};
diff --git a/src/ts/types/cover.ts b/src/ts/types/cover.ts
deleted file mode 100644
index 4df898e..0000000
--- a/src/ts/types/cover.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export type Cover = {
- altText: string;
- sourceUrl: string;
- title: string;
-} | null;
-
-export type RawCover = {
- node: Cover;
-} | null;
diff --git a/src/ts/types/mdx.ts b/src/ts/types/mdx.ts
new file mode 100644
index 0000000..16538c1
--- /dev/null
+++ b/src/ts/types/mdx.ts
@@ -0,0 +1,17 @@
+import { StaticImageData } from 'next/image';
+import { Meta } from './app';
+
+export type MDXData = {
+ file: string;
+ image: StaticImageData;
+};
+
+export type MDXPageMeta = Pick<Meta<'page'>, 'cover' | 'dates' | 'seo'> & {
+ intro: string;
+ title: string;
+};
+
+export type MDXProjectMeta = Exclude<Meta<'project'>, 'wordsCount'> & {
+ intro: string;
+ title: string;
+};
diff --git a/src/ts/types/nav.ts b/src/ts/types/nav.ts
deleted file mode 100644
index 7cfc46b..0000000
--- a/src/ts/types/nav.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export type NavItem = {
- id: string;
- name: string;
- slug: string;
-};
diff --git a/src/ts/types/prism.ts b/src/ts/types/prism.ts
deleted file mode 100644
index 663bc08..0000000
--- a/src/ts/types/prism.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-export type PrismLanguages =
- | 'apacheconf'
- | 'bash'
- | 'css'
- | 'diff'
- | 'docker'
- | 'editorconfig'
- | 'ejs'
- | 'git'
- | 'graphql'
- | 'html'
- | 'ignore'
- | 'ini'
- | 'javascript'
- | 'jsdoc'
- | 'json'
- | 'jsx'
- | 'makefile'
- | 'markup'
- | 'php'
- | 'phpdoc'
- | 'regex'
- | 'scss'
- | 'shell-session'
- | 'smarty'
- | 'tcl'
- | 'toml'
- | 'tsx'
- | 'twig'
- | 'yaml';
-
-export type PrismDefaultPlugins =
- | 'autoloader'
- | 'color-scheme'
- | 'copy-to-clipboard'
- | 'match-braces'
- | 'normalize-whitespace'
- | 'show-language'
- | 'toolbar';
-
-export type PrismPlugins =
- | 'command-line'
- | 'diff-highlight'
- | 'inline-color'
- | 'line-highlight'
- | 'line-numbers';
-
-export type PrismProviderProps = {
- language: PrismLanguages;
- plugins: PrismPlugins[];
-};
diff --git a/src/ts/types/raw-data.ts b/src/ts/types/raw-data.ts
new file mode 100644
index 0000000..dc3db90
--- /dev/null
+++ b/src/ts/types/raw-data.ts
@@ -0,0 +1,105 @@
+/**
+ * Types for raw data coming from GraphQL API.
+ */
+
+import { NodeResponse, PageInfo } from '@services/graphql/api';
+import { ContentKind } from './app';
+
+export type ACFPosts = {
+ postsInThematic?: RawThematicPreview[];
+ postsInTopic?: RawTopicPreview[];
+};
+
+export type ACFThematics = {
+ postsInThematic: RawArticle[];
+};
+
+export type ACFTopics = {
+ officialWebsite: string;
+ postsInTopic: RawArticle[];
+};
+
+export type ContentParts = {
+ afterMore: string;
+ beforeMore: string;
+};
+
+export type Info = {
+ wordsCount: number;
+};
+
+export type RawAuthor<T extends ContentKind> = {
+ description?: T extends 'comment' ? never : string;
+ gravatarUrl?: string;
+ name: string;
+ url?: string;
+};
+
+export type RawComment = {
+ approved: boolean;
+ author: NodeResponse<RawAuthor<'comment'>>;
+ content: string;
+ databaseId: number;
+ date: string;
+ parentDatabaseId: number;
+};
+
+export type RawCover = {
+ altText: string;
+ mediaDetails: {
+ width: number;
+ height: number;
+ };
+ sourceUrl: string;
+ title?: string;
+};
+
+export type RawArticle = RawPage & {
+ acfPosts: ACFPosts;
+ commentCount: number | null;
+};
+
+export type RawArticlePreview = Pick<
+ RawArticle,
+ 'databaseId' | 'date' | 'featuredImage' | 'slug' | 'title'
+>;
+
+export type RawPage = {
+ author?: NodeResponse<RawAuthor<'page'>>;
+ contentParts: ContentParts;
+ databaseId: number;
+ date: string;
+ featuredImage: NodeResponse<RawCover> | null;
+ info: Info;
+ modified: string;
+ seo?: RawSEO;
+ slug: string;
+ title: string;
+};
+
+export type RawSEO = {
+ metaDesc: string;
+ title: string;
+};
+
+export type RawThematic = RawPage & {
+ acfThematics: ACFThematics;
+};
+
+export type RawThematicPreview = Pick<
+ RawThematic,
+ 'databaseId' | 'featuredImage' | 'slug' | 'title'
+>;
+
+export type RawTopic = RawPage & {
+ acfTopics: ACFTopics;
+};
+
+export type RawTopicPreview = Pick<
+ RawTopic,
+ 'databaseId' | 'featuredImage' | 'slug' | 'title'
+>;
+
+export type TotalItems = {
+ pageInfo: Pick<PageInfo, 'total'>;
+};
diff --git a/src/ts/types/repos.ts b/src/ts/types/repos.ts
deleted file mode 100644
index 7dacacc..0000000
--- a/src/ts/types/repos.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export type RepoData = {
- created_at: string;
- updated_at: string;
- stargazers_count: number;
-};
-
-export type RepoAPI = 'github';
diff --git a/src/ts/types/seo.ts b/src/ts/types/seo.ts
deleted file mode 100644
index 18e3c95..0000000
--- a/src/ts/types/seo.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export type SEO = {
- title: string;
- metaDesc: string;
- metaRobotsNofollow: string;
- metaRobotsNoindex: string;
-};
diff --git a/src/ts/types/swr.ts b/src/ts/types/swr.ts
new file mode 100644
index 0000000..4da6b2c
--- /dev/null
+++ b/src/ts/types/swr.ts
@@ -0,0 +1,5 @@
+export type SWRResult<T> = {
+ data?: T;
+ isLoading: boolean;
+ isError: boolean;
+};
diff --git a/src/ts/types/taxonomies.ts b/src/ts/types/taxonomies.ts
deleted file mode 100644
index 17fc022..0000000
--- a/src/ts/types/taxonomies.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { ContentInfo, ContentParts, Dates, Slug } from './app';
-import { ArticlePreview, RawArticlePreview } from './articles';
-import { Cover, RawCover } from './cover';
-import { SEO } from './seo';
-
-//==============================================================================
-// Taxonomies base
-//==============================================================================
-
-type Taxonomy = {
- content: string;
- databaseId: number;
- dates: Dates;
- id: string;
- info: ContentInfo;
- intro: string;
- posts: ArticlePreview[];
- seo: SEO;
- title: string;
-};
-
-type TaxonomyPreview = Pick<
- Taxonomy,
- 'databaseId' | 'id' | 'info' | 'seo' | 'title'
-> & {
- slug: string;
-};
-
-//==============================================================================
-// Topics
-//==============================================================================
-
-export type Topic = Taxonomy & {
- featuredImage: Cover;
- officialWebsite: string;
-};
-
-export type RawTopicPreview = TaxonomyPreview & {
- featuredImage: RawCover;
-};
-
-export type TopicPreview = TaxonomyPreview & {
- featuredImage: Cover;
-};
-
-export type AllTopics = {
- topics: {
- nodes: TopicPreview[];
- };
-};
-
-export type RawTopic = TopicPreview & {
- acfTopics: {
- officialWebsite: string;
- postsInTopic: RawArticlePreview[];
- };
- contentParts: ContentParts;
- date: string;
- featuredImage: RawCover;
- modified: string;
-};
-
-export type TopicBy = {
- topic: RawTopic;
-};
-
-export type AllTopicsSlug = {
- topics: {
- nodes: Slug[];
- };
-};
-
-export type TopicProps = {
- allTopics: TopicPreview[];
- topic: Topic;
-};
-
-//==============================================================================
-// Thematics
-//==============================================================================
-
-export type Thematic = Taxonomy;
-
-export type ThematicPreview = TaxonomyPreview;
-
-export type AllThematics = {
- thematics: {
- nodes: ThematicPreview[];
- };
-};
-
-export type RawThematic = TaxonomyPreview & {
- acfThematics: {
- postsInThematic: RawArticlePreview[];
- };
- contentParts: ContentParts;
- date: string;
- modified: string;
-};
-
-export type ThematicBy = {
- thematic: RawThematic;
-};
-
-export type AllThematicsSlug = {
- thematics: {
- nodes: Slug[];
- };
-};
-
-export type ThematicProps = {
- allThematics: ThematicPreview[];
- thematic: Thematic;
-};
diff --git a/src/utils/config.ts b/src/utils/config.ts
index 874a24c..61a46b4 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -18,8 +18,9 @@ export const settings = {
},
copyright: {
startYear: '2012',
- endYear: new Date().getFullYear(),
+ endYear: new Date().getFullYear().toString(),
},
+ email: process.env.APP_AUTHOR_EMAIL || '',
locales: {
defaultLocale: 'fr',
defaultCountry: 'FR',
diff --git a/src/utils/helpers/author.ts b/src/utils/helpers/author.ts
new file mode 100644
index 0000000..40743ca
--- /dev/null
+++ b/src/utils/helpers/author.ts
@@ -0,0 +1,32 @@
+import { type Author, type ContentKind } from '@ts/types/app';
+import { type RawAuthor } from '@ts/types/raw-data';
+
+/**
+ * Convert author raw data to regular data.
+ *
+ * @param {RawAuthor<ContentKind>} data - The author raw data.
+ * @param {ContentKind} kind - The author kind. Either `page` or `comment`.
+ * @param {number} [avatarSize] - The author avatar size.
+ * @returns {Author<ContentKind>} The author data.
+ */
+export const getAuthorFromRawData = (
+ data: RawAuthor<typeof kind>,
+ kind: ContentKind,
+ avatarSize: number = 80
+): Author<typeof kind> => {
+ const { name, description, gravatarUrl, url } = data;
+
+ return {
+ name,
+ avatar: gravatarUrl
+ ? {
+ alt: `${name} avatar`,
+ height: avatarSize,
+ src: gravatarUrl,
+ width: avatarSize,
+ }
+ : undefined,
+ description,
+ website: url,
+ };
+};
diff --git a/src/utils/helpers/dates.ts b/src/utils/helpers/dates.ts
new file mode 100644
index 0000000..cb56ad2
--- /dev/null
+++ b/src/utils/helpers/dates.ts
@@ -0,0 +1,40 @@
+import { settings } from '@utils/config';
+
+/**
+ * Format a date based on a locale.
+ *
+ * @param {string} date - The date.
+ * @param {string} [locale] - A locale.
+ * @returns {string} The locale date string.
+ */
+export const getFormattedDate = (
+ date: string,
+ locale: string = settings.locales.defaultLocale
+): string => {
+ const dateOptions: Intl.DateTimeFormatOptions = {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ };
+
+ return new Date(date).toLocaleDateString(locale, dateOptions);
+};
+
+/**
+ * Format a time based on a locale.
+ *
+ * @param {string} time - The time.
+ * @param {string} [locale] - A locale.
+ * @returns {string} The locale time string.
+ */
+export const getFormattedTime = (
+ time: string,
+ locale: string = settings.locales.defaultLocale
+): string => {
+ const formattedTime = new Date(time).toLocaleTimeString(locale, {
+ hour: 'numeric',
+ minute: 'numeric',
+ });
+
+ return locale === 'fr' ? formattedTime.replace(':', 'h') : formattedTime;
+};
diff --git a/src/utils/helpers/format.ts b/src/utils/helpers/format.ts
deleted file mode 100644
index dd35868..0000000
--- a/src/utils/helpers/format.ts
+++ /dev/null
@@ -1,310 +0,0 @@
-import { ParamsIds, ParamsSlug, Slug } from '@ts/types/app';
-import {
- Article,
- ArticlePreview,
- RawArticle,
- RawArticlePreview,
-} from '@ts/types/articles';
-import { Comment, RawComment } from '@ts/types/comments';
-import {
- RawTopic,
- RawTopicPreview,
- RawThematic,
- Topic,
- TopicPreview,
- Thematic,
-} from '@ts/types/taxonomies';
-
-/**
- * Format a post preview from RawArticlePreview to ArticlePreview type.
- * @param rawPost - A post preview coming from WP GraphQL.
- * @returns A formatted post preview.
- */
-export const getFormattedPostPreview = (rawPost: RawArticlePreview) => {
- const {
- acfPosts,
- commentCount,
- contentParts,
- date,
- featuredImage,
- id,
- info,
- modified,
- slug,
- title,
- } = rawPost;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const topics = acfPosts.postsInTopic ? acfPosts.postsInTopic : [];
- const thematics = acfPosts.postsInThematic ? acfPosts.postsInThematic : [];
-
- const formattedPost: ArticlePreview = {
- commentCount,
- dates,
- featuredImage: featuredImage ? featuredImage.node : null,
- id,
- info,
- intro: contentParts.beforeMore,
- slug,
- topics,
- thematics,
- title,
- };
-
- return formattedPost;
-};
-
-/**
- * Format an array of posts list from RawArticlePreview to ArticlePreview type.
- * @param rawPosts - A posts list coming from WP GraphQL.
- * @returns A formatted posts list.
- */
-export const getFormattedPostsList = (
- rawPosts: RawArticlePreview[]
-): ArticlePreview[] => {
- return rawPosts
- .filter((post) => Object.getOwnPropertyNames(post).length > 0)
- .map((post) => {
- return getFormattedPostPreview(post);
- });
-};
-
-/**
- * Format a topic from RawTopic to Topic type.
- * @param rawTopic - A topic coming from WP GraphQL.
- * @returns A formatted topic.
- */
-export const getFormattedTopic = (rawTopic: RawTopic): Topic => {
- const {
- acfTopics,
- contentParts,
- databaseId,
- date,
- featuredImage,
- id,
- info,
- modified,
- seo,
- title,
- } = rawTopic;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const posts = getFormattedPostsList(acfTopics.postsInTopic);
-
- const formattedTopic: Topic = {
- content: contentParts.afterMore,
- databaseId,
- dates,
- featuredImage: featuredImage ? featuredImage.node : null,
- id,
- info,
- intro: contentParts.beforeMore,
- officialWebsite: acfTopics.officialWebsite,
- posts,
- seo,
- title,
- };
-
- return formattedTopic;
-};
-
-/**
- * Format a thematic from RawThematic to Thematic type.
- * @param rawThematic - A thematic coming from wP GraphQL.
- * @returns A formatted thematic.
- */
-export const getFormattedThematic = (rawThematic: RawThematic): Thematic => {
- const {
- acfThematics,
- contentParts,
- databaseId,
- date,
- id,
- info,
- modified,
- seo,
- title,
- } = rawThematic;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const posts = getFormattedPostsList(acfThematics.postsInThematic);
-
- const formattedThematic: Thematic = {
- content: contentParts.afterMore,
- databaseId,
- dates,
- id,
- info,
- intro: contentParts.beforeMore,
- posts,
- seo,
- title,
- };
-
- return formattedThematic;
-};
-
-/**
- * Format a comments list from RawComment to Comment type.
- * @param rawComments - A comments list coming from WP GraphQL.
- * @returns A formatted comments list.
- */
-export const getFormattedComments = (rawComments: RawComment[]): Comment[] => {
- const formattedComments: Comment[] = rawComments.map((comment) => {
- const formattedComment: Comment = {
- ...comment,
- author: comment.author.node,
- replies: [],
- };
-
- return formattedComment;
- });
-
- return formattedComments;
-};
-
-/**
- * Create a comments tree with replies.
- * @param comments - A flatten comments list.
- * @returns An array of comments with replies.
- */
-export const buildCommentsTree = (comments: Comment[]) => {
- type CommentsHashTable = {
- [key: string]: Comment;
- };
-
- const hashTable: CommentsHashTable = Object.create(null);
- const commentsTree: Comment[] = [];
-
- comments.forEach(
- (comment) => (hashTable[comment.databaseId] = { ...comment, replies: [] })
- );
-
- comments.forEach((comment) => {
- if (!comment.parentDatabaseId) {
- commentsTree.push(hashTable[comment.databaseId]);
- } else {
- hashTable[comment.parentDatabaseId].replies.push(
- hashTable[comment.databaseId]
- );
- }
- });
-
- return commentsTree;
-};
-
-export const getFormattedTopicsPreview = (
- topics: RawTopicPreview[]
-): TopicPreview[] => {
- const formattedTopics: TopicPreview[] = topics.map((topic) => {
- return {
- ...topic,
- featuredImage: topic.featuredImage ? topic.featuredImage.node : null,
- };
- });
-
- return formattedTopics;
-};
-
-/**
- * Format an article from RawArticle to Article type.
- * @param rawPost - An article coming from WP GraphQL.
- * @returns A formatted article.
- */
-export const getFormattedPost = (rawPost: RawArticle): Article => {
- const {
- acfPosts,
- author,
- commentCount,
- contentParts,
- databaseId,
- date,
- featuredImage,
- id,
- info,
- modified,
- seo,
- title,
- } = rawPost;
-
- const dates = {
- publication: date,
- update: modified,
- };
-
- const topics = acfPosts.postsInTopic
- ? getFormattedTopicsPreview(acfPosts.postsInTopic)
- : [];
-
- const formattedPost: Article = {
- author: author.node,
- commentCount,
- content: contentParts.afterMore,
- databaseId,
- dates,
- featuredImage: featuredImage ? featuredImage.node : null,
- id,
- info,
- intro: contentParts.beforeMore,
- seo,
- topics,
- thematics: acfPosts.postsInThematic ? acfPosts.postsInThematic : [],
- title,
- };
-
- return formattedPost;
-};
-
-/**
- * Converts a date to a string by using the specified locale.
- * @param {string} date The date.
- * @param {string} locale A locale.
- * @returns {string} The formatted date to locale date string.
- */
-export const getFormattedDate = (date: string, locale: string) => {
- const dateOptions: Intl.DateTimeFormatOptions = {
- day: 'numeric',
- month: 'long',
- year: 'numeric',
- };
-
- return new Date(date).toLocaleDateString(locale, dateOptions);
-};
-
-/**
- * Convert an array of slugs to an array of params with slug.
- * @param {Slug} array - An array of object with slug.
- * @returns {ParamsSlug} An array of params with slug.
- */
-export const getFormattedPaths = (array: Slug[]): ParamsSlug[] => {
- return array.map((object) => {
- return { params: { slug: object.slug } };
- });
-};
-
-/**
- * Convert a number of pages to an array of params with ids.
- * @param {number} totalPages - The total pages.
- * @returns {ParamsIds} An array of params with ids.
- */
-export const getFormattedPageNumbers = (totalPages: number): ParamsIds[] => {
- const paths = [];
-
- for (let i = 1; i <= totalPages; i++) {
- paths.push({ params: { id: `${i}` } });
- }
-
- return paths;
-};
diff --git a/src/utils/helpers/i18n.ts b/src/utils/helpers/i18n.ts
index c4734ad..5d19c8c 100644
--- a/src/utils/helpers/i18n.ts
+++ b/src/utils/helpers/i18n.ts
@@ -3,7 +3,7 @@ import { settings } from '@utils/config';
import { readFile } from 'fs/promises';
import path from 'path';
-type Messages = { [key: string]: string };
+export type Messages = { [key: string]: string };
export const defaultLocale = settings.locales.defaultLocale;
diff --git a/src/utils/helpers/images.ts b/src/utils/helpers/images.ts
new file mode 100644
index 0000000..30bb8be
--- /dev/null
+++ b/src/utils/helpers/images.ts
@@ -0,0 +1,18 @@
+import { Image } from '@ts/types/app';
+import { RawCover } from '@ts/types/raw-data';
+
+/**
+ * Retrieve an Image object from raw data.
+ *
+ * @param image - The cover raw data.
+ * @returns {Image} - An Image object.
+ */
+export const getImageFromRawData = (image: RawCover): Image => {
+ return {
+ alt: image.altText,
+ height: image.mediaDetails.height,
+ src: image.sourceUrl,
+ title: image.title,
+ width: image.mediaDetails.width,
+ };
+};
diff --git a/src/utils/helpers/pages.ts b/src/utils/helpers/pages.ts
new file mode 100644
index 0000000..773d454
--- /dev/null
+++ b/src/utils/helpers/pages.ts
@@ -0,0 +1,85 @@
+import { type Post } from '@components/organisms/layout/posts-list';
+import { type LinksListItems } from '@components/organisms/widgets/links-list-widget';
+import { type EdgesResponse } from '@services/graphql/api';
+import { getArticleFromRawData } from '@services/graphql/articles';
+import { type Article, type PageLink } from '@ts/types/app';
+import {
+ type RawArticle,
+ type RawThematicPreview,
+ type RawTopicPreview,
+} from '@ts/types/raw-data';
+import { getImageFromRawData } from './images';
+
+/**
+ * Convert raw data to a Link object.
+ *
+ * @param data - An object.
+ * @param {number} data.databaseId - The data id.
+ * @param {number} [data.logo] - The data logo.
+ * @param {string} data.slug - The data slug.
+ * @param {string} data.title - The data name.
+ * @returns {PageLink} The link data (id, slug and title).
+ */
+export const getPageLinkFromRawData = (
+ data: RawThematicPreview | RawTopicPreview,
+ kind: 'thematic' | 'topic'
+): PageLink => {
+ const { databaseId, featuredImage, slug, title } = data;
+ const baseUrl = kind === 'thematic' ? '/thematique/' : '/sujet/';
+
+ return {
+ id: databaseId,
+ logo: featuredImage ? getImageFromRawData(featuredImage?.node) : undefined,
+ name: title,
+ url: `${baseUrl}${slug}`,
+ };
+};
+
+/**
+ * Convert page link data to an array of links items.
+ *
+ * @param {PageLink[]} links - An array of page links.
+ * @returns {LinksListItem[]} An array of links items.
+ */
+export const getLinksListItems = (links: PageLink[]): LinksListItems[] => {
+ return links.map((link) => {
+ return {
+ name: link.name,
+ url: link.url,
+ };
+ });
+};
+
+/**
+ * Retrieve the posts list with the article URL.
+ *
+ * @param {Article[]} posts - An array of articles.
+ * @returns {Post[]} An array of posts with full article URL.
+ */
+export const getPostsWithUrl = (posts: Article[]): Post[] => {
+ return posts.map((post) => {
+ return {
+ ...post,
+ url: `/article/${post.slug}`,
+ };
+ });
+};
+
+/**
+ * Retrieve the posts list from raw data.
+ *
+ * @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
+ * @returns {Post[]} An array of posts.
+ */
+export const getPostsList = (rawData: EdgesResponse<RawArticle>[]): Post[] => {
+ const articlesList: RawArticle[] = [];
+ rawData.forEach((articleData) =>
+ articleData.edges.forEach((edge) => {
+ articlesList.push(edge.node);
+ })
+ );
+
+ return getPostsWithUrl(
+ articlesList.map((article) => getArticleFromRawData(article))
+ );
+};
diff --git a/src/utils/helpers/prism.ts b/src/utils/helpers/prism.ts
deleted file mode 100644
index a5f5787..0000000
--- a/src/utils/helpers/prism.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Check if the current block has a defined language.
- * @param classList - A list of class.
- * @returns {boolean} - True if a class starts with "language-".
- */
-const isLanguageBlock = (classList: DOMTokenList) => {
- const classes = Array.from(classList);
- return classes.some((className) => /language-.*/.test(className));
-};
-
-/**
- * Add automatically some classes and attributes for PrismJs.
- *
- * These classes and attributes are needed by Prism or to customize comments.
- */
-export const addPrismClasses = () => {
- const preTags = document.getElementsByTagName('pre');
-
- Array.from(preTags).forEach((preTag) => {
- if (!isLanguageBlock(preTag.classList)) return;
-
- preTag.classList.add('match-braces');
-
- if (preTag.classList.contains('filter-output')) {
- preTag.setAttribute('data-filter-output', '#output#');
- }
-
- if (preTag.classList.contains('language-bash')) {
- preTag.classList.add('command-line');
- } else if (!preTag.classList.contains('language-diff')) {
- preTag.classList.add('line-numbers');
- }
- });
-};
diff --git a/src/utils/helpers/projects.ts b/src/utils/helpers/projects.ts
index 1612dae..a0f0c04 100644
--- a/src/utils/helpers/projects.ts
+++ b/src/utils/helpers/projects.ts
@@ -1,36 +1,55 @@
-import { Project, ProjectMeta } from '@ts/types/app';
+import { ProjectCard, ProjectPreview } from '@ts/types/app';
+import { MDXProjectMeta } from '@ts/types/mdx';
import { readdirSync } from 'fs';
import path from 'path';
/**
- * Retrieve project's data by id.
- * @param {string} id - The filename without extension.
- * @returns {Promise<Project>} - The project data.
+ * Retrieve all the projects filename.
+ *
+ * @returns {string[]} An array of filenames.
*/
-export const getProjectData = async (id: string): Promise<Project> => {
+export const getProjectFilenames = (): string[] => {
+ const projectsDirectory = path.join(process.cwd(), 'src/content/projects');
+ const filenames = readdirSync(projectsDirectory);
+
+ return filenames.map((filename) => filename.replace(/\.mdx$/, ''));
+};
+
+/**
+ * Retrieve the data of a project by filename.
+ *
+ * @param {string} filename - The project filename.
+ * @returns {Promise<ProjectPreview>}
+ */
+export const getProjectData = async (
+ filename: string
+): Promise<ProjectPreview> => {
try {
const {
- intro,
meta,
- seo,
- tagline,
}: {
- intro: string;
- meta: ProjectMeta & { title: string };
- seo: { title: string; description: string };
- tagline?: string;
- } = await import(`../../content/projects/${id}.mdx`);
+ meta: MDXProjectMeta;
+ } = await import(`../../content/projects/${filename}.mdx`);
- const { title, ...onlyMeta } = meta;
+ const { dates, intro, title, ...projectMeta } = meta;
+ const { publication, update } = dates;
+ const cover = await import(`../../../public/projects/${filename}.jpg`);
return {
- id,
- intro: intro || '',
- meta: onlyMeta || {},
- slug: id,
- title,
- seo: seo || {},
- tagline: tagline || '',
+ id: filename,
+ intro,
+ meta: {
+ ...projectMeta,
+ dates: { publication, update },
+ // Dynamic import source does not work so I use it only to get sizes
+ cover: {
+ ...cover.default,
+ alt: `${title} image`,
+ src: `/projects/${filename}.jpg`,
+ },
+ },
+ slug: filename,
+ title: title,
};
} catch (err) {
console.error(err);
@@ -39,48 +58,44 @@ export const getProjectData = async (id: string): Promise<Project> => {
};
/**
- * Retrieve the projects data from filenames.
- * @param {string[]} filenames - An array of filenames.
- * @returns {Promise<Project[]>} An array of projects with meta.
+ * Retrieve all the projects data using filenames.
+ *
+ * @param {string[]} filenames - The filenames without extension.
+ * @returns {Promise<ProjectCard[]>} - An array of projects data.
*/
-const getProjectsWithMeta = async (filenames: string[]): Promise<Project[]> => {
+export const getProjectsData = async (
+ filenames: string[]
+): Promise<ProjectCard[]> => {
return Promise.all(
filenames.map(async (filename) => {
- return getProjectData(filename);
+ const { id, meta, slug, title } = await getProjectData(filename);
+ const { cover, dates, tagline, technologies } = meta;
+ return { id, meta: { cover, dates, tagline, technologies }, slug, title };
})
);
};
/**
* Method to sort an array of projects by publication date.
- * @param {Project} a - A single project.
- * @param {Project} b - A single project.
+ *
+ * @param {ProjectCard} a - A single project.
+ * @param {ProjectCard} b - A single project.
* @returns The result used by Array.sort() method: 1 || -1 || 0.
*/
-const sortProjectByPublicationDate = (a: Project, b: Project) => {
- if (a.meta.publishedOn < b.meta.publishedOn) return 1;
- if (a.meta.publishedOn > b.meta.publishedOn) return -1;
+const sortProjectsByPublicationDate = (a: ProjectCard, b: ProjectCard) => {
+ if (a.meta.dates.publication < b.meta.dates.publication) return 1;
+ if (a.meta.dates.publication > b.meta.dates.publication) return -1;
return 0;
};
/**
- * Retrieve all the projects filename.
- * @returns {string[]} An array of filenames.
- */
-export const getAllProjectsFilename = (): string[] => {
- const projectsDirectory = path.join(process.cwd(), 'src/content/projects');
- const filenames = readdirSync(projectsDirectory);
-
- return filenames.map((filename) => filename.replace(/\.mdx$/, ''));
-};
-
-/**
* Retrieve all projects in content folder sorted by publication date.
- * @returns {Promise<Project[]>} An array of projects.
+ *
+ * @returns {Promise<ProjectCard[]>} An array of projects.
*/
-export const getSortedProjects = async (): Promise<Project[]> => {
- const filenames = getAllProjectsFilename();
- const projects = await getProjectsWithMeta(filenames);
+export const getProjectsCard = async (): Promise<ProjectCard[]> => {
+ const filenames = getProjectFilenames();
+ const projects = await getProjectsData(filenames);
- return [...projects].sort(sortProjectByPublicationDate);
+ return [...projects].sort(sortProjectsByPublicationDate);
};
diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts
index 10a8e77..8ee774c 100644
--- a/src/utils/helpers/rss.ts
+++ b/src/utils/helpers/rss.ts
@@ -1,20 +1,35 @@
-import { getPostsTotal, getPublishedPosts } from '@services/graphql/queries';
-import { ArticlePreview } from '@ts/types/articles';
-import { PostsList } from '@ts/types/blog';
+import {
+ getArticleFromRawData,
+ getArticles,
+ getTotalArticles,
+} from '@services/graphql/articles';
+import { Article } from '@ts/types/app';
import { settings } from '@utils/config';
import { Feed } from 'feed';
-const getAllPosts = async (): Promise<ArticlePreview[]> => {
- const totalPosts = await getPostsTotal();
- const posts: ArticlePreview[] = [];
+/**
+ * Retrieve the data for all the articles.
+ *
+ * @returns {Promise<Article[]>} - All the articles.
+ */
+const getAllArticles = async (): Promise<Article[]> => {
+ const totalArticles = await getTotalArticles();
+ const rawArticles = await getArticles({ first: totalArticles });
+ const articles: Article[] = [];
- const postsList: PostsList = await getPublishedPosts({ first: totalPosts });
- posts.push(...postsList.posts);
+ rawArticles.edges.forEach((edge) =>
+ articles.push(getArticleFromRawData(edge.node))
+ );
- return posts;
+ return articles;
};
-export const generateFeed = async () => {
+/**
+ * Generate a new feed.
+ *
+ * @returns {Promise<Feed>} - The feed.
+ */
+export const generateFeed = async (): Promise<Feed> => {
const author = {
name: settings.name,
email: process.env.APP_AUTHOR_EMAIL,
@@ -38,16 +53,16 @@ export const generateFeed = async () => {
title,
});
- const posts = await getAllPosts();
+ const articles = await getAllArticles();
- posts.forEach((post) => {
+ articles.forEach((article) => {
feed.addItem({
- content: post.intro,
- date: new Date(post.dates.publication),
- description: post.intro,
- id: post.id,
- link: `${settings.url}/article/${post.slug}`,
- title: post.title,
+ content: article.intro,
+ date: new Date(article.meta!.dates.publication),
+ description: article.intro,
+ id: `${article.id}`,
+ link: `${settings.url}/article/${article.slug}`,
+ title: article.title,
});
});
diff --git a/src/utils/helpers/schema-org.ts b/src/utils/helpers/schema-org.ts
new file mode 100644
index 0000000..cdace00
--- /dev/null
+++ b/src/utils/helpers/schema-org.ts
@@ -0,0 +1,224 @@
+import { Dates } from '@ts/types/app';
+import { settings } from '@utils/config';
+import {
+ AboutPage,
+ Article,
+ Blog,
+ BlogPosting,
+ ContactPage,
+ Graph,
+ WebPage,
+} from 'schema-dts';
+
+export type GetBlogSchemaProps = {
+ /**
+ * True if the page is part of the blog.
+ */
+ isSinglePage: boolean;
+ /**
+ * The page locale.
+ */
+ locale: string;
+ /**
+ * The page slug with a leading slash.
+ */
+ slug: string;
+};
+
+/**
+ * Retrieve the JSON for Blog schema.
+ *
+ * @param props - The page data.
+ * @returns {Blog} The JSON for Blog schema.
+ */
+export const getBlogSchema = ({
+ isSinglePage,
+ locale,
+ slug,
+}: GetBlogSchemaProps): Blog => {
+ return {
+ '@id': `${settings.url}/#blog`,
+ '@type': 'Blog',
+ author: { '@id': `${settings.url}/#branding` },
+ creator: { '@id': `${settings.url}/#branding` },
+ editor: { '@id': `${settings.url}/#branding` },
+ blogPost: isSinglePage ? { '@id': `${settings.url}/#article` } : undefined,
+ inLanguage: locale,
+ isPartOf: isSinglePage
+ ? {
+ '@id': `${settings.url}${slug}`,
+ }
+ : undefined,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ mainEntityOfPage: isSinglePage
+ ? undefined
+ : { '@id': `${settings.url}${slug}` },
+ };
+};
+
+export type SinglePageSchemaReturn = {
+ about: AboutPage;
+ contact: ContactPage;
+ page: Article;
+ post: BlogPosting;
+};
+
+export type SinglePageSchemaKind = keyof SinglePageSchemaReturn;
+
+export type GetSinglePageSchemaProps<T extends SinglePageSchemaKind> = {
+ /**
+ * The number of comments.
+ */
+ commentsCount?: number;
+ /**
+ * The page content.
+ */
+ content?: string;
+ /**
+ * The url of the cover.
+ */
+ cover?: string;
+ /**
+ * The page dates.
+ */
+ dates: Dates;
+ /**
+ * The page description.
+ */
+ description: string;
+ /**
+ * The page id.
+ */
+ id: string;
+ /**
+ * The page kind.
+ */
+ kind: T;
+ /**
+ * The page locale.
+ */
+ locale: string;
+ /**
+ * The page slug with a leading slash.
+ */
+ slug: string;
+ /**
+ * The page title.
+ */
+ title: string;
+};
+
+/**
+ * Retrieve the JSON schema depending on the page kind.
+ *
+ * @param props - The page data.
+ * @returns {SinglePageSchemaReturn[T]} - Either AboutPage, ContactPage, Article or BlogPosting schema.
+ */
+export const getSinglePageSchema = <T extends SinglePageSchemaKind>({
+ commentsCount,
+ content,
+ cover,
+ dates,
+ description,
+ id,
+ kind,
+ locale,
+ title,
+ slug,
+}: GetSinglePageSchemaProps<T>): SinglePageSchemaReturn[T] => {
+ const publicationDate = new Date(dates.publication);
+ const updateDate = dates.update ? new Date(dates.update) : undefined;
+ const singlePageSchemaType = {
+ about: 'AboutPage',
+ contact: 'ContactPage',
+ page: 'Article',
+ post: 'BlogPosting',
+ };
+
+ return {
+ '@id': `${settings.url}/#${id}`,
+ '@type': singlePageSchemaType[kind],
+ name: title,
+ description,
+ articleBody: content,
+ author: { '@id': `${settings.url}/#branding` },
+ commentCount: commentsCount,
+ copyrightYear: publicationDate.getFullYear(),
+ creator: { '@id': `${settings.url}/#branding` },
+ dateCreated: publicationDate.toISOString(),
+ dateModified: updateDate && updateDate.toISOString(),
+ datePublished: publicationDate.toISOString(),
+ editor: { '@id': `${settings.url}/#branding` },
+ headline: title,
+ image: cover,
+ inLanguage: locale,
+ license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+ thumbnailUrl: cover,
+ isPartOf:
+ kind === 'post'
+ ? {
+ '@id': `${settings.url}/blog`,
+ }
+ : undefined,
+ mainEntityOfPage: { '@id': `${settings.url}${slug}` },
+ } as SinglePageSchemaReturn[T];
+};
+
+export type GetWebPageSchemaProps = {
+ /**
+ * The page description.
+ */
+ description: string;
+ /**
+ * The page locale.
+ */
+ locale: string;
+ /**
+ * The page slug.
+ */
+ slug: string;
+ /**
+ * The page title.
+ */
+ title: string;
+ /**
+ * The page last update.
+ */
+ updateDate?: string;
+};
+
+/**
+ * Retrieve the JSON for WebPage schema.
+ *
+ * @param props - The page data.
+ * @returns {WebPage} The JSON for WebPage schema.
+ */
+export const getWebPageSchema = ({
+ description,
+ locale,
+ slug,
+ title,
+ updateDate,
+}: GetWebPageSchemaProps): WebPage => {
+ return {
+ '@id': `${settings.url}${slug}`,
+ '@type': 'WebPage',
+ breadcrumb: { '@id': `${settings.url}/#breadcrumb` },
+ lastReviewed: updateDate,
+ name: title,
+ description: description,
+ inLanguage: locale,
+ reviewedBy: { '@id': `${settings.url}/#branding` },
+ url: `${settings.url}${slug}`,
+ isPartOf: {
+ '@id': `${settings.url}`,
+ },
+ };
+};
+
+export const getSchemaJson = (graphs: Graph['@graph']): Graph => {
+ return {
+ '@context': 'https://schema.org',
+ '@graph': graphs,
+ };
+};
diff --git a/src/utils/helpers/slugify.ts b/src/utils/helpers/slugify.ts
deleted file mode 100644
index 55ff583..0000000
--- a/src/utils/helpers/slugify.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Convert a text into a slug or id.
- * https://gist.github.com/codeguy/6684588#gistcomment-3332719
- *
- * @param {string} text Text to slugify.
- */
-export const slugify = (text: string) => {
- return text
- .toString()
- .normalize('NFD')
- .replace(/[\u0300-\u036f]/g, '')
- .toLowerCase()
- .trim()
- .replace(/\s+/g, '-')
- .replace(/[^\w\-]+/g, '-')
- .replace(/\-\-+/g, '-')
- .replace(/(^-)|(-$)/g, '');
-};
diff --git a/src/utils/helpers/sort.ts b/src/utils/helpers/sort.ts
deleted file mode 100644
index c1ee35d..0000000
--- a/src/utils/helpers/sort.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ArticlePreview } from '@ts/types/articles';
-import { PostsList } from '@ts/types/blog';
-
-type YearCollection = {
- [key: string]: ArticlePreview[];
-};
-
-export const sortPostsByYear = (data: PostsList[]) => {
- const yearCollection: YearCollection = {};
-
- data.forEach((page) => {
- page.posts.forEach((post) => {
- const postYear = new Date(post.dates.publication)
- .getFullYear()
- .toString();
- yearCollection[postYear] = [...(yearCollection[postYear] || []), post];
- });
- });
-
- return yearCollection;
-};
diff --git a/src/utils/helpers/strings.ts b/src/utils/helpers/strings.ts
new file mode 100644
index 0000000..1af0ca2
--- /dev/null
+++ b/src/utils/helpers/strings.ts
@@ -0,0 +1,39 @@
+/**
+ * Convert a text into a slug or id.
+ * https://gist.github.com/codeguy/6684588#gistcomment-3332719
+ *
+ * @param {string} text - A text to slugify.
+ * @returns {string} The slug.
+ */
+export const slugify = (text: string): string => {
+ return text
+ .toString()
+ .normalize('NFD')
+ .replace(/[\u0300-\u036f]/g, '')
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, '-')
+ .replace(/[^\w\-]+/g, '-')
+ .replace(/\-\-+/g, '-')
+ .replace(/(^-)|(-$)/g, '');
+};
+
+/**
+ * Capitalize the first letter of a string.
+ *
+ * @param {string} text - A text to capitalize.
+ * @returns {string} The capitalized text.
+ */
+export const capitalize = (text: string): string => {
+ return text.replace(/^\w/, (firstLetter) => firstLetter.toUpperCase());
+};
+
+/**
+ * Convert a text from kebab case (foo-bar) to camel case (fooBar).
+ *
+ * @param {string} text - A text to transform.
+ * @returns {string} The text in camel case.
+ */
+export const fromKebabCaseToCamelCase = (text: string): string => {
+ return text.replace(/-./g, (x) => x[1].toUpperCase());
+};
diff --git a/src/utils/hooks/use-add-classname.tsx b/src/utils/hooks/use-add-classname.tsx
new file mode 100644
index 0000000..0584084
--- /dev/null
+++ b/src/utils/hooks/use-add-classname.tsx
@@ -0,0 +1,34 @@
+import { useCallback, useEffect } from 'react';
+
+export type UseAddClassNameProps = {
+ className: string;
+ element?: HTMLElement;
+ elements?: NodeListOf<HTMLElement> | HTMLElement[];
+};
+
+/**
+ * Add className to the given element(s).
+ *
+ * @param {UseAddClassNameProps} props - An object with classnames and one or more elements.
+ */
+const useAddClassName = ({
+ className,
+ element,
+ elements,
+}: UseAddClassNameProps) => {
+ const classNames = className.split(' ').filter((string) => string !== '');
+
+ const setClassName = useCallback(
+ (el: HTMLElement) => {
+ el.classList.add(...classNames);
+ },
+ [classNames]
+ );
+
+ useEffect(() => {
+ if (element) setClassName(element);
+ if (elements && elements.length > 0) elements.forEach(setClassName);
+ }, [element, elements, setClassName]);
+};
+
+export default useAddClassName;
diff --git a/src/utils/hooks/use-attributes.tsx b/src/utils/hooks/use-attributes.tsx
new file mode 100644
index 0000000..6d18048
--- /dev/null
+++ b/src/utils/hooks/use-attributes.tsx
@@ -0,0 +1,52 @@
+import { fromKebabCaseToCamelCase } from '@utils/helpers/strings';
+import { useCallback, useEffect } from 'react';
+
+export type useAttributesProps = {
+ /**
+ * An HTML element.
+ */
+ element?: HTMLElement;
+ /**
+ * A node list of HTML Element.
+ */
+ elements?: NodeListOf<HTMLElement> | HTMLElement[];
+ /**
+ * The attribute name.
+ */
+ attribute: string;
+ /**
+ * The attribute value.
+ */
+ value: string;
+};
+
+/**
+ * Set HTML attributes to the given element or to the HTML document.
+ *
+ * @param props - An object with element, attribute name and value.
+ */
+const useAttributes = ({
+ element,
+ elements,
+ attribute,
+ value,
+}: useAttributesProps) => {
+ const setAttribute = useCallback(
+ (el: HTMLElement) => {
+ if (attribute.startsWith('data')) {
+ el.setAttribute(attribute, value);
+ } else {
+ const camelCaseAttribute = fromKebabCaseToCamelCase(attribute);
+ el.dataset[camelCaseAttribute] = value;
+ }
+ },
+ [attribute, value]
+ );
+
+ useEffect(() => {
+ if (element) setAttribute(element);
+ if (elements && elements.length > 0) elements.forEach(setAttribute);
+ }, [element, elements, setAttribute]);
+};
+
+export default useAttributes;
diff --git a/src/utils/hooks/use-breadcrumb.tsx b/src/utils/hooks/use-breadcrumb.tsx
new file mode 100644
index 0000000..130ebf1
--- /dev/null
+++ b/src/utils/hooks/use-breadcrumb.tsx
@@ -0,0 +1,107 @@
+import { BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
+import { slugify } from '@utils/helpers/strings';
+import { useIntl } from 'react-intl';
+import { BreadcrumbList } from 'schema-dts';
+import useSettings from './use-settings';
+
+export type useBreadcrumbProps = {
+ /**
+ * The current page title.
+ */
+ title: string;
+ /**
+ * The current page url.
+ */
+ url: string;
+};
+
+export type useBreadcrumbReturn = {
+ /**
+ * The breadcrumb items.
+ */
+ items: BreadcrumbItem[];
+ /**
+ * The breadcrumb JSON schema.
+ */
+ schema: BreadcrumbList['itemListElement'][];
+};
+
+/**
+ * Retrieve the breadcrumb items.
+ *
+ * @param {useBreadcrumbProps} props - An object (the current page title & url).
+ * @returns {useBreadcrumbReturn} The breadcrumb items and its JSON schema.
+ */
+const useBreadcrumb = ({
+ title,
+ url,
+}: useBreadcrumbProps): useBreadcrumbReturn => {
+ const intl = useIntl();
+ const { website } = useSettings();
+ const isArticle = url.startsWith('/article/');
+ const isHome = url === '/';
+ const isPageNumber = url.includes('/page/');
+ const isProject = url.startsWith('/projets/');
+ const isSearch = url.startsWith('/recherche');
+ const isThematic = url.startsWith('/thematique/');
+ const isTopic = url.startsWith('/sujet/');
+
+ const homeLabel = intl.formatMessage({
+ defaultMessage: 'Home',
+ description: 'Breadcrumb: home label',
+ id: 'j5k9Fe',
+ });
+ const items: BreadcrumbItem[] = [{ id: 'home', name: homeLabel, url: '/' }];
+ const schema: BreadcrumbList['itemListElement'][] = [
+ {
+ '@type': 'ListItem',
+ position: 1,
+ name: homeLabel,
+ item: website.url,
+ },
+ ];
+
+ if (isHome) return { items, schema };
+
+ if (isArticle || isPageNumber || isSearch || isThematic || isTopic) {
+ const blogLabel = intl.formatMessage({
+ defaultMessage: 'Blog',
+ description: 'Breadcrumb: blog label',
+ id: 'Es52wh',
+ });
+ items.push({ id: 'blog', name: blogLabel, url: '/blog' });
+ schema.push({
+ '@type': 'ListItem',
+ position: 2,
+ name: blogLabel,
+ item: `${website.url}/blog`,
+ });
+ }
+
+ if (isProject) {
+ const projectsLabel = intl.formatMessage({
+ defaultMessage: 'Projects',
+ description: 'Breadcrumb: projects label',
+ id: '28GZdv',
+ });
+ items.push({ id: 'blog', name: projectsLabel, url: '/projets' });
+ schema.push({
+ '@type': 'ListItem',
+ position: 2,
+ name: projectsLabel,
+ item: `${website.url}/projets`,
+ });
+ }
+
+ items.push({ id: slugify(title), name: title, url });
+ schema.push({
+ '@type': 'ListItem',
+ position: schema.length + 1,
+ name: title,
+ item: `${website.url}${url}`,
+ });
+
+ return { items, schema };
+};
+
+export default useBreadcrumb;
diff --git a/src/utils/hooks/use-click-outside.tsx b/src/utils/hooks/use-click-outside.tsx
new file mode 100644
index 0000000..cead98b
--- /dev/null
+++ b/src/utils/hooks/use-click-outside.tsx
@@ -0,0 +1,46 @@
+import { RefObject, useCallback, useEffect } from 'react';
+
+/**
+ * Listen for click/focus outside an element and execute the given callback.
+ *
+ * @param el - A React reference to an element.
+ * @param callback - A callback function to execute on click outside.
+ */
+const useClickOutside = (
+ el: RefObject<HTMLElement>,
+ callback: (target: EventTarget) => void
+) => {
+ /**
+ * Check if an event target is outside an element.
+ *
+ * @param {RefObject<HTMLElement>} ref - A React reference object.
+ * @param {EventTarget} target - An event target.
+ * @returns {boolean} True if the event target is outside the ref object.
+ */
+ const isTargetOutside = (
+ ref: RefObject<HTMLElement>,
+ target: EventTarget
+ ): boolean => {
+ if (!ref.current) return false;
+ return !ref.current.contains(target as Node);
+ };
+
+ const handleEvent = useCallback(
+ (e: MouseEvent | FocusEvent) => {
+ if (e.target && isTargetOutside(el, e.target)) callback(e.target);
+ },
+ [el, callback]
+ );
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleEvent);
+ document.addEventListener('focusin', handleEvent);
+
+ return () => {
+ document.removeEventListener('mousedown', handleEvent);
+ document.removeEventListener('focusin', handleEvent);
+ };
+ }, [handleEvent]);
+};
+
+export default useClickOutside;
diff --git a/src/utils/hooks/use-data-from-api.tsx b/src/utils/hooks/use-data-from-api.tsx
new file mode 100644
index 0000000..7082941
--- /dev/null
+++ b/src/utils/hooks/use-data-from-api.tsx
@@ -0,0 +1,23 @@
+import { useEffect, useState } from 'react';
+
+/**
+ * Fetch data from an API.
+ *
+ * This hook is a wrapper to `setState` + `useEffect`.
+ *
+ * @param fetcher - A function to fetch data from API.
+ * @returns {T | undefined} The requested data.
+ */
+const useDataFromAPI = <T extends unknown>(
+ fetcher: () => Promise<T>
+): T | undefined => {
+ const [data, setData] = useState<T>();
+
+ useEffect(() => {
+ fetcher().then((apiData) => setData(apiData));
+ }, [fetcher]);
+
+ return data;
+};
+
+export default useDataFromAPI;
diff --git a/src/utils/hooks/useGithubApi.tsx b/src/utils/hooks/use-github-api.tsx
index 4b0b3b2..edff974 100644
--- a/src/utils/hooks/useGithubApi.tsx
+++ b/src/utils/hooks/use-github-api.tsx
@@ -1,15 +1,22 @@
-import { RepoData } from '@ts/types/repos';
+import { SWRResult } from '@ts/types/swr';
import useSWR, { Fetcher } from 'swr';
+export type RepoData = {
+ created_at: string;
+ updated_at: string;
+ stargazers_count: number;
+};
+
const fetcher: Fetcher<RepoData, string> = (...args) =>
fetch(...args).then((res) => res.json());
/**
* Retrieve data from Github API.
- * @param repo The repo name. Format: "User/project-slug".
- * @returns {object} The data and two booleans to determine if is loading/error.
+ *
+ * @param repo - The Github repo (`owner/repo-name`).
+ * @returns The repository data.
*/
-const useGithubApi = (repo: string) => {
+const useGithubApi = (repo: string): SWRResult<RepoData> => {
const apiUrl = repo ? `https://api.github.com/repos/${repo}` : null;
const { data, error } = useSWR<RepoData>(apiUrl, fetcher);
diff --git a/src/utils/hooks/useHeadingsTree.tsx b/src/utils/hooks/use-headings-tree.tsx
index f2be406..4646b4a 100644
--- a/src/utils/hooks/useHeadingsTree.tsx
+++ b/src/utils/hooks/use-headings-tree.tsx
@@ -1,40 +1,71 @@
-import { Heading } from '@ts/types/app';
-import { slugify } from '@utils/helpers/slugify';
-import { useRouter } from 'next/router';
+import { slugify } from '@utils/helpers/strings';
import { useCallback, useEffect, useMemo, useState } from 'react';
-const useHeadingsTree = (wrapper: string) => {
- const router = useRouter();
+export type Heading = {
+ /**
+ * The heading depth.
+ */
+ depth: number;
+ /**
+ * The heading id.
+ */
+ id: string;
+ /**
+ * The heading children.
+ */
+ children: Heading[];
+ /**
+ * The heading title.
+ */
+ title: string;
+};
+
+/**
+ * Get the headings tree of the given HTML element.
+ *
+ * @param {HTMLElement} wrapper - An HTML element that contains the headings.
+ * @returns {Heading[]} The headings tree.
+ */
+const useHeadingsTree = (wrapper: HTMLElement): Heading[] => {
const depths = useMemo(() => ['h2', 'h3', 'h4', 'h5', 'h6'], []);
const [allHeadings, setAllHeadings] =
useState<NodeListOf<HTMLHeadingElement>>();
+ const [headingsTree, setHeadingsTree] = useState<Heading[]>([]);
useEffect(() => {
- const query = depths
- .map((depth) => `${wrapper} > *:not(aside, #comments) ${depth}`)
- .join(', ');
+ const query = depths.join(', ');
const result: NodeListOf<HTMLHeadingElement> =
- document.querySelectorAll(query);
+ wrapper.querySelectorAll(query);
setAllHeadings(result);
- }, [depths, wrapper, router.asPath]);
-
- const [headingsTree, setHeadingsTree] = useState<Heading[]>([]);
-
- const getElementDepth = useCallback(
- (el: HTMLHeadingElement) => {
+ }, [depths, wrapper]);
+
+ const getDepth = useCallback(
+ /**
+ * Retrieve the heading element depth.
+ *
+ * @param {HTMLHeadingElement} el - An heading element.
+ * @returns {number} The heading depth.
+ */
+ (el: HTMLHeadingElement): number => {
return depths.findIndex((depth) => depth === el.localName);
},
[depths]
);
const formatHeadings = useCallback(
+ /**
+ * Convert a list of headings into an array of Heading objects.
+ *
+ * @param {NodeListOf<HTMLHeadingElement>} headings - A list of headings.
+ * @returns {Heading[]} An array of Heading objects.
+ */
(headings: NodeListOf<HTMLHeadingElement>): Heading[] => {
const formattedHeadings: Heading[] = [];
Array.from(headings).forEach((heading) => {
const title: string = heading.textContent!;
const id = slugify(title);
- const depth = getElementDepth(heading);
+ const depth = getDepth(heading);
const children: Heading[] = [];
heading.id = id;
@@ -49,10 +80,16 @@ const useHeadingsTree = (wrapper: string) => {
return formattedHeadings;
},
- [getElementDepth]
+ [getDepth]
);
const buildSubTree = useCallback(
+ /**
+ * Build the heading subtree.
+ *
+ * @param {Heading} parent - The heading parent.
+ * @param {Heading} currentHeading - The current heading element.
+ */
(parent: Heading, currentHeading: Heading): void => {
if (parent.depth === currentHeading.depth - 1) {
parent.children.push(currentHeading);
@@ -65,6 +102,12 @@ const useHeadingsTree = (wrapper: string) => {
);
const buildTree = useCallback(
+ /**
+ * Build a heading tree.
+ *
+ * @param {Heading[]} headings - An array of Heading objects.
+ * @returns {Heading[]} The headings tree.
+ */
(headings: Heading[]): Heading[] => {
const tree: Heading[] = [];
@@ -82,7 +125,13 @@ const useHeadingsTree = (wrapper: string) => {
[buildSubTree]
);
- const getHeadingsList = useCallback(
+ const getHeadingsTree = useCallback(
+ /**
+ * Retrieve a headings tree from a list of headings element.
+ *
+ * @param {NodeListOf<HTMLHeadingElement>} headings - A headings list.
+ * @returns {Heading[]} The headings tree.
+ */
(headings: NodeListOf<HTMLHeadingElement>): Heading[] => {
const formattedHeadings = formatHeadings(headings);
@@ -93,10 +142,10 @@ const useHeadingsTree = (wrapper: string) => {
useEffect(() => {
if (allHeadings) {
- const headingsList = getHeadingsList(allHeadings);
+ const headingsList = getHeadingsTree(allHeadings);
setHeadingsTree(headingsList);
}
- }, [allHeadings, getHeadingsList]);
+ }, [allHeadings, getHeadingsTree]);
return headingsTree;
};
diff --git a/src/utils/hooks/use-input-autofocus.tsx b/src/utils/hooks/use-input-autofocus.tsx
new file mode 100644
index 0000000..c7700e9
--- /dev/null
+++ b/src/utils/hooks/use-input-autofocus.tsx
@@ -0,0 +1,39 @@
+import { RefObject, useEffect } from 'react';
+
+export type UseInputAutofocusProps = {
+ /**
+ * The focus condition. True give focus to the input.
+ */
+ condition: boolean;
+ /**
+ * An optional delay. Default: 0.
+ */
+ delay?: number;
+ /**
+ * A reference to the input element.
+ */
+ ref: RefObject<HTMLInputElement>;
+};
+
+/**
+ * Set focus on an input with an optional delay.
+ */
+const useInputAutofocus = ({
+ condition,
+ delay = 0,
+ ref,
+}: UseInputAutofocusProps) => {
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (ref.current && condition) {
+ ref.current.focus();
+ }
+ }, delay);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [condition, delay, ref]);
+};
+
+export default useInputAutofocus;
diff --git a/src/utils/hooks/use-is-mounted.tsx b/src/utils/hooks/use-is-mounted.tsx
new file mode 100644
index 0000000..ca79afb
--- /dev/null
+++ b/src/utils/hooks/use-is-mounted.tsx
@@ -0,0 +1,19 @@
+import { RefObject, useEffect, useState } from 'react';
+
+/**
+ * Check if an HTML element is mounted.
+ *
+ * @param {RefObject<HTMLElement>} ref - A React reference to an HTML element.
+ * @returns {boolean} True if the HTML element is mounted.
+ */
+const useIsMounted = (ref: RefObject<HTMLElement>) => {
+ const [isMounted, setIsMounted] = useState<boolean>(false);
+
+ useEffect(() => {
+ if (ref.current) setIsMounted(true);
+ }, [ref]);
+
+ return isMounted;
+};
+
+export default useIsMounted;
diff --git a/src/utils/hooks/use-local-storage.tsx b/src/utils/hooks/use-local-storage.tsx
new file mode 100644
index 0000000..da0292b
--- /dev/null
+++ b/src/utils/hooks/use-local-storage.tsx
@@ -0,0 +1,35 @@
+import { LocalStorage } from '@services/local-storage';
+import { Dispatch, SetStateAction, useEffect, useState } from 'react';
+
+export type UseLocalStorageReturn<T> = {
+ value: T;
+ setValue: Dispatch<SetStateAction<T>>;
+};
+
+/**
+ * Use the local storage.
+ *
+ * @param {string} key - The storage local key.
+ * @param {T} [fallbackValue] - A fallback value if local storage is empty.
+ * @returns {UseLocalStorageReturn<T>} An object with value and setValue.
+ */
+const useLocalStorage = <T extends unknown>(
+ key: string,
+ fallbackValue: T
+): UseLocalStorageReturn<T> => {
+ const getInitialValue = () => {
+ if (typeof window === 'undefined') return fallbackValue;
+ const storedValue = LocalStorage.get<T>(key);
+ return storedValue || fallbackValue;
+ };
+
+ const [value, setValue] = useState<T>(getInitialValue);
+
+ useEffect(() => {
+ LocalStorage.set(key, value);
+ }, [key, value]);
+
+ return { value, setValue };
+};
+
+export default useLocalStorage;
diff --git a/src/utils/hooks/use-pagination.tsx b/src/utils/hooks/use-pagination.tsx
new file mode 100644
index 0000000..a80a539
--- /dev/null
+++ b/src/utils/hooks/use-pagination.tsx
@@ -0,0 +1,117 @@
+import { type EdgesResponse, type EdgesVars } from '@services/graphql/api';
+import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite';
+
+export type UsePaginationProps<T> = {
+ /**
+ * The initial data.
+ */
+ fallbackData: EdgesResponse<T>[];
+ /**
+ * A function to fetch more data.
+ */
+ fetcher: (props: EdgesVars) => Promise<EdgesResponse<T>>;
+ /**
+ * The number of results per page.
+ */
+ perPage: number;
+ /**
+ * An optional search string.
+ */
+ search?: string;
+};
+
+export type UsePaginationReturn<T> = {
+ /**
+ * The data from the API.
+ */
+ data?: EdgesResponse<T>[];
+ /**
+ * An error thrown by fetcher.
+ */
+ error: any;
+ /**
+ * Determine if there's more data to fetch.
+ */
+ hasNextPage?: boolean;
+ /**
+ * Determine if the initial data is loading.
+ */
+ isLoadingInitialData: boolean;
+ /**
+ * Determine if more data is currently loading.
+ */
+ isLoadingMore?: boolean;
+ /**
+ * Determine if the data is refreshing.
+ */
+ isRefreshing?: boolean;
+ /**
+ * Determine if there's a request or revalidation loading.
+ */
+ isValidating: boolean;
+ /**
+ * Set the number of pages that need to be fetched.
+ */
+ setSize: (
+ size: number | ((_size: number) => number)
+ ) => Promise<EdgesResponse<T>[] | undefined>;
+};
+
+/**
+ * Handle data fetching with pagination.
+ *
+ * This hook is a wrapper of `useSWRInfinite` hook.
+ *
+ * @param {UsePaginationProps} props - The pagination configuration.
+ * @returns {UsePaginationReturn} An object with pagination data and helpers.
+ */
+const usePagination = <T extends object>({
+ fallbackData,
+ fetcher,
+ perPage,
+ search,
+}: UsePaginationProps<T>): UsePaginationReturn<T> => {
+ const getKey: SWRInfiniteKeyLoader = (
+ pageIndex: number,
+ previousData: EdgesResponse<T>
+ ): EdgesVars | null => {
+ // Reached the end.
+ if (previousData && !previousData.edges.length) return null;
+
+ // Fetch data using this parameters.
+ return pageIndex === 0
+ ? { first: perPage, search }
+ : {
+ first: perPage,
+ after: previousData.pageInfo.endCursor,
+ search,
+ };
+ };
+
+ const { data, error, isValidating, size, setSize } = useSWRInfinite(
+ getKey,
+ fetcher,
+ { fallbackData }
+ );
+
+ const isLoadingInitialData = !data && !error;
+ const isLoadingMore =
+ isLoadingInitialData ||
+ (size > 0 && data && typeof data[size - 1] === 'undefined');
+ const isRefreshing = isValidating && data && data.length === size;
+ const hasNextPage =
+ data && data.length > 0 && data[data.length - 1].pageInfo.hasNextPage;
+
+ return {
+ data,
+ error,
+ hasNextPage,
+ isLoadingInitialData,
+ isLoadingMore,
+ isRefreshing,
+ isValidating,
+ setSize,
+ };
+};
+
+export default usePagination;
diff --git a/src/utils/hooks/use-prism.tsx b/src/utils/hooks/use-prism.tsx
new file mode 100644
index 0000000..ef1a4c8
--- /dev/null
+++ b/src/utils/hooks/use-prism.tsx
@@ -0,0 +1,182 @@
+import Prism from 'prismjs';
+import { useEffect, useMemo } from 'react';
+import { useIntl } from 'react-intl';
+
+const PRISM_PLUGINS = [
+ 'autoloader',
+ 'color-scheme',
+ 'command-line',
+ 'copy-to-clipboard',
+ 'diff-highlight',
+ 'inline-color',
+ 'line-highlight',
+ 'line-numbers',
+ 'match-braces',
+ 'normalize-whitespace',
+ 'show-language',
+ 'toolbar',
+] as const;
+
+export type PrismPlugin = typeof PRISM_PLUGINS[number];
+
+export type DefaultPrismPlugin = Extract<
+ PrismPlugin,
+ | 'autoloader'
+ | 'color-scheme'
+ | 'copy-to-clipboard'
+ | 'match-braces'
+ | 'normalize-whitespace'
+ | 'show-language'
+ | 'toolbar'
+>;
+
+export type OptionalPrismPlugin = Exclude<PrismPlugin, DefaultPrismPlugin>;
+
+export type PrismLanguage =
+ | 'apacheconf'
+ | 'bash'
+ | 'css'
+ | 'diff'
+ | 'docker'
+ | 'editorconfig'
+ | 'ejs'
+ | 'git'
+ | 'graphql'
+ | 'html'
+ | 'ignore'
+ | 'ini'
+ | 'javascript'
+ | 'jsdoc'
+ | 'json'
+ | 'jsx'
+ | 'makefile'
+ | 'markup'
+ | 'php'
+ | 'phpdoc'
+ | 'regex'
+ | 'scss'
+ | 'shell-session'
+ | 'smarty'
+ | 'tcl'
+ | 'toml'
+ | 'tsx'
+ | 'twig'
+ | 'yaml';
+
+export type PrismAttributes = {
+ 'data-prismjs-copy': string;
+ 'data-prismjs-copy-success': string;
+ 'data-prismjs-copy-error': string;
+ 'data-prismjs-color-scheme-dark': string;
+ 'data-prismjs-color-scheme-light': string;
+};
+
+export type UsePrismProps = {
+ language?: PrismLanguage;
+ plugins: OptionalPrismPlugin[];
+};
+
+export type UsePrismReturn = {
+ attributes: PrismAttributes;
+ className: string;
+};
+
+/**
+ * Import and configure all given Prism plugins.
+ *
+ * @param {PrismPlugin[]} plugins - The Prism plugins to activate.
+ */
+const loadPrismPlugins = async (plugins: PrismPlugin[]) => {
+ for (const plugin of plugins) {
+ try {
+ if (plugin === 'color-scheme') {
+ await import(`@utils/plugins/prism-${plugin}`);
+ } else {
+ await import(`prismjs/plugins/${plugin}/prism-${plugin}.min.js`);
+ }
+
+ if (plugin === 'autoloader') {
+ Prism.plugins.autoloader.languages_path = '/prism/';
+ }
+ } catch (error) {
+ console.error('usePrism: an error occurred while loading Prism plugins.');
+ console.error(error);
+ }
+ }
+};
+
+/**
+ * Use Prism and its plugins.
+ *
+ * @param {UsePrismProps} props - An object of options.
+ * @returns {UsePrismReturn} An object of data.
+ */
+const usePrism = ({ language, plugins }: UsePrismProps): UsePrismReturn => {
+ /**
+ * The order matter. Toolbar must be loaded before some other plugins.
+ */
+ const defaultPlugins: DefaultPrismPlugin[] = useMemo(
+ () => [
+ 'toolbar',
+ 'autoloader',
+ 'show-language',
+ 'copy-to-clipboard',
+ 'color-scheme',
+ 'match-braces',
+ 'normalize-whitespace',
+ ],
+ []
+ );
+
+ useEffect(() => {
+ loadPrismPlugins([...defaultPlugins, ...plugins]).then(() => {
+ Prism.highlightAll();
+ });
+ }, [defaultPlugins, plugins]);
+
+ const defaultClassName = 'match-braces';
+ const languageClassName = language ? `language-${language}` : '';
+ const pluginsClassName = plugins.join(' ');
+ const className = `${defaultClassName} ${pluginsClassName} ${languageClassName}`;
+
+ const intl = useIntl();
+ const copyText = intl.formatMessage({
+ defaultMessage: 'Copy',
+ description: 'usePrism: copy button text (not clicked)',
+ id: '6GySNl',
+ });
+ const copiedText = intl.formatMessage({
+ defaultMessage: 'Copied!',
+ description: 'usePrism: copy button text (clicked)',
+ id: 'nsw6Th',
+ });
+ const errorText = intl.formatMessage({
+ defaultMessage: 'Use Ctrl+c to copy',
+ description: 'usePrism: copy button error text',
+ id: 'lKhTGM',
+ });
+ const darkTheme = intl.formatMessage({
+ defaultMessage: 'Dark Theme 🌙',
+ description: 'usePrism: toggle dark theme button text',
+ id: 'QLisK6',
+ });
+ const lightTheme = intl.formatMessage({
+ defaultMessage: 'Light Theme 🌞',
+ description: 'usePrism: toggle light theme button text',
+ id: 'hHVgW3',
+ });
+ const attributes = {
+ 'data-prismjs-copy': copyText,
+ 'data-prismjs-copy-success': copiedText,
+ 'data-prismjs-copy-error': errorText,
+ 'data-prismjs-color-scheme-dark': darkTheme,
+ 'data-prismjs-color-scheme-light': lightTheme,
+ };
+
+ return {
+ attributes,
+ className,
+ };
+};
+
+export default usePrism;
diff --git a/src/utils/hooks/use-query-selector-all.tsx b/src/utils/hooks/use-query-selector-all.tsx
new file mode 100644
index 0000000..6ac8a08
--- /dev/null
+++ b/src/utils/hooks/use-query-selector-all.tsx
@@ -0,0 +1,24 @@
+import { useRouter } from 'next/router';
+import { useEffect, useState } from 'react';
+
+/**
+ * Use `document.querySelectorAll`.
+ *
+ * @param {string} query - A query.
+ * @returns {NodeListOf<HTMLElementTagNameMap[T]|undefined>} - The node list.
+ */
+const useQuerySelectorAll = <T extends keyof HTMLElementTagNameMap>(
+ query: string
+): NodeListOf<HTMLElementTagNameMap[T]> | undefined => {
+ const [elements, setElements] =
+ useState<NodeListOf<HTMLElementTagNameMap[T]>>();
+ const { asPath } = useRouter();
+
+ useEffect(() => {
+ setElements(document.querySelectorAll(query));
+ }, [asPath, query]);
+
+ return elements;
+};
+
+export default useQuerySelectorAll;
diff --git a/src/utils/hooks/use-reading-time.tsx b/src/utils/hooks/use-reading-time.tsx
new file mode 100644
index 0000000..fb54135
--- /dev/null
+++ b/src/utils/hooks/use-reading-time.tsx
@@ -0,0 +1,58 @@
+import { useIntl } from 'react-intl';
+
+/**
+ * Retrieve the estimated reading time by words count.
+ *
+ * @param {number} wordsCount - The number of words.
+ * @returns {string} The estimated reading time.
+ */
+const useReadingTime = (
+ wordsCount: number,
+ onlyMinutes: boolean = false
+): string => {
+ const intl = useIntl();
+ const wordsPerMinute = 245;
+ const wordsPerSecond = wordsPerMinute / 60;
+ const estimatedTimeInSeconds = wordsCount / wordsPerSecond;
+
+ if (onlyMinutes) {
+ const estimatedTimeInMinutes = Math.round(estimatedTimeInSeconds / 60);
+
+ return intl.formatMessage(
+ {
+ defaultMessage: '{minutesCount} minutes',
+ description: 'useReadingTime: rounded minutes count',
+ id: 's1i43J',
+ },
+ { minutesCount: estimatedTimeInMinutes }
+ );
+ } else {
+ const estimatedTimeInMinutes = Math.floor(estimatedTimeInSeconds / 60);
+
+ if (estimatedTimeInMinutes <= 0) {
+ return intl.formatMessage(
+ {
+ defaultMessage: '{count} seconds',
+ description: 'useReadingTime: seconds count',
+ id: 'i7Wq3G',
+ },
+ { count: estimatedTimeInSeconds.toFixed(0) }
+ );
+ }
+
+ const remainingSeconds = Math.round(
+ estimatedTimeInSeconds - estimatedTimeInMinutes * 60
+ ).toFixed(0);
+
+ return intl.formatMessage(
+ {
+ defaultMessage: '{minutesCount} minutes {secondsCount} seconds',
+ description: 'useReadingTime: minutes + seconds count',
+ id: 'OevMeU',
+ },
+ { minutesCount: estimatedTimeInMinutes, secondsCount: remainingSeconds }
+ );
+ }
+};
+
+export default useReadingTime;
diff --git a/src/utils/hooks/use-redirection.tsx b/src/utils/hooks/use-redirection.tsx
new file mode 100644
index 0000000..9eb26c2
--- /dev/null
+++ b/src/utils/hooks/use-redirection.tsx
@@ -0,0 +1,33 @@
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+export type RouterQuery = {
+ param: string;
+ value: string;
+};
+
+export type UseRedirectionProps = {
+ /**
+ * The router query.
+ */
+ query: RouterQuery;
+ /**
+ * The redirection url.
+ */
+ redirectTo: string;
+};
+
+/**
+ * Redirect to another url when router query match the given parameters.
+ *
+ * @param {UseRedirectionProps} props - The redirection parameters.
+ */
+const useRedirection = ({ query, redirectTo }: UseRedirectionProps) => {
+ const router = useRouter();
+
+ useEffect(() => {
+ if (router.query[query.param] === query.value) router.push(redirectTo);
+ }, [query, redirectTo, router]);
+};
+
+export default useRedirection;
diff --git a/src/utils/hooks/use-route-change.tsx b/src/utils/hooks/use-route-change.tsx
new file mode 100644
index 0000000..82e01a1
--- /dev/null
+++ b/src/utils/hooks/use-route-change.tsx
@@ -0,0 +1,12 @@
+import { useRouter } from 'next/router';
+import { useEffect } from 'react';
+
+const useRouteChange = (callback: () => void) => {
+ const { events } = useRouter();
+
+ useEffect(() => {
+ events.on('routeChangeStart', callback);
+ }, [events, callback]);
+};
+
+export default useRouteChange;
diff --git a/src/utils/hooks/use-scroll-position.tsx b/src/utils/hooks/use-scroll-position.tsx
new file mode 100644
index 0000000..47cfdd0
--- /dev/null
+++ b/src/utils/hooks/use-scroll-position.tsx
@@ -0,0 +1,15 @@
+import { useEffect } from 'react';
+
+/**
+ * Execute the given function based on scroll position.
+ *
+ * @param scrollHandler - A callback function.
+ */
+const useScrollPosition = (scrollHandler: () => void) => {
+ useEffect(() => {
+ window.addEventListener('scroll', scrollHandler);
+ return () => window.removeEventListener('scroll', scrollHandler);
+ }, [scrollHandler]);
+};
+
+export default useScrollPosition;
diff --git a/src/utils/hooks/use-settings.tsx b/src/utils/hooks/use-settings.tsx
new file mode 100644
index 0000000..cc5261b
--- /dev/null
+++ b/src/utils/hooks/use-settings.tsx
@@ -0,0 +1,118 @@
+import photo from '@assets/images/armand-philippot.jpg';
+import { settings } from '@utils/config';
+import { useRouter } from 'next/router';
+
+export type BlogSettings = {
+ /**
+ * The number of posts per page.
+ */
+ postsPerPage: number;
+};
+
+export type CopyrightSettings = {
+ /**
+ * The copyright end year.
+ */
+ end: string;
+ /**
+ * The copyright start year.
+ */
+ start: string;
+};
+
+export type LocaleSettings = {
+ /**
+ * The default locale.
+ */
+ default: string;
+ /**
+ * The supported locales.
+ */
+ supported: string[];
+};
+
+export type PictureSettings = {
+ /**
+ * The picture height.
+ */
+ height: number;
+ /**
+ * The picture url.
+ */
+ src: string;
+ /**
+ * The picture width.
+ */
+ width: number;
+};
+
+export type WebsiteSettings = {
+ /**
+ * The website name.
+ */
+ name: string;
+ /**
+ * The website baseline.
+ */
+ baseline: string;
+ /**
+ * The website copyright dates.
+ */
+ copyright: CopyrightSettings;
+ /**
+ * The website admin email.
+ */
+ email: string;
+ /**
+ * The website locales.
+ */
+ locales: LocaleSettings;
+ /**
+ * A picture representing the website.
+ */
+ picture: PictureSettings;
+ /**
+ * The website url.
+ */
+ url: string;
+};
+
+export type UseSettingsReturn = {
+ blog: BlogSettings;
+ website: WebsiteSettings;
+};
+
+/**
+ * Retrieve the website and blog settings.
+ *
+ * @returns {UseSettingsReturn} - An object describing settings.
+ */
+const useSettings = (): UseSettingsReturn => {
+ const { baseline, copyright, email, locales, name, postsPerPage, url } =
+ settings;
+ const router = useRouter();
+ const locale = router.locale || locales.defaultLocale;
+
+ return {
+ blog: {
+ postsPerPage,
+ },
+ website: {
+ baseline: locale.startsWith('en') ? baseline.en : baseline.fr,
+ copyright: {
+ end: copyright.endYear,
+ start: copyright.startYear,
+ },
+ email,
+ locales: {
+ default: locales.defaultLocale,
+ supported: locales.supported,
+ },
+ name,
+ picture: photo,
+ url,
+ },
+ };
+};
+
+export default useSettings;
diff --git a/src/utils/hooks/use-styles.tsx b/src/utils/hooks/use-styles.tsx
new file mode 100644
index 0000000..d47e9fb
--- /dev/null
+++ b/src/utils/hooks/use-styles.tsx
@@ -0,0 +1,29 @@
+import { RefObject, useEffect } from 'react';
+
+export type UseStylesProps = {
+ /**
+ * A property name or a CSS variable.
+ */
+ property: string;
+ /**
+ * The styles.
+ */
+ styles: string;
+ /**
+ * A targeted element reference.
+ */
+ target: RefObject<HTMLElement>;
+};
+
+/**
+ * Add styles to an element using a React reference.
+ *
+ * @param {UseStylesProps} props - An object with property, styles and target.
+ */
+const useStyles = ({ property, styles, target }: UseStylesProps) => {
+ useEffect(() => {
+ if (target.current) target.current.style.setProperty(property, styles);
+ }, [property, styles, target]);
+};
+
+export default useStyles;
diff --git a/src/utils/hooks/use-update-ackee-options.tsx b/src/utils/hooks/use-update-ackee-options.tsx
new file mode 100644
index 0000000..7c1d98a
--- /dev/null
+++ b/src/utils/hooks/use-update-ackee-options.tsx
@@ -0,0 +1,19 @@
+import { useAckeeTracker } from '@utils/providers/ackee';
+import { useEffect } from 'react';
+
+export type AckeeOptions = 'full' | 'partial';
+
+/**
+ * Update Ackee settings with the given choice.
+ *
+ * @param {AckeeOptions} value - Either `full` or `partial`.
+ */
+const useUpdateAckeeOptions = (value: AckeeOptions) => {
+ const { setDetailed } = useAckeeTracker();
+
+ useEffect(() => {
+ setDetailed(value === 'full');
+ }, [value, setDetailed]);
+};
+
+export default useUpdateAckeeOptions;
diff --git a/src/utils/providers/ackee.tsx b/src/utils/providers/ackee.tsx
index c103668..0cb0166 100644
--- a/src/utils/providers/ackee.tsx
+++ b/src/utils/providers/ackee.tsx
@@ -1,5 +1,5 @@
import { useRouter } from 'next/router';
-import { createContext, FC, useContext, useState } from 'react';
+import { createContext, FC, ReactNode, useContext, useState } from 'react';
import useAckee from 'use-ackee';
export type AckeeProps = {
@@ -10,6 +10,7 @@ export type AckeeProps = {
};
export type AckeeProviderProps = {
+ children: ReactNode;
domain: string;
siteId: string;
ignoreLocalhost?: boolean;
diff --git a/src/utils/providers/prism-theme.tsx b/src/utils/providers/prism-theme.tsx
index 2ed8454..dd8feb7 100644
--- a/src/utils/providers/prism-theme.tsx
+++ b/src/utils/providers/prism-theme.tsx
@@ -1,7 +1,10 @@
-import { LocalStorage } from '@services/local-storage';
+import useAttributes from '@utils/hooks/use-attributes';
+import useLocalStorage from '@utils/hooks/use-local-storage';
+import useQuerySelectorAll from '@utils/hooks/use-query-selector-all';
import {
createContext,
FC,
+ ReactNode,
useCallback,
useContext,
useEffect,
@@ -9,7 +12,7 @@ import {
} from 'react';
export type PrismTheme = 'dark' | 'light' | 'system';
-export type ResolvedPrismTheme = 'dark' | 'light';
+export type ResolvedPrismTheme = Exclude<PrismTheme, 'system'>;
export type UsePrismThemeProps = {
themes: PrismTheme[];
@@ -17,11 +20,11 @@ export type UsePrismThemeProps = {
setTheme: (theme: PrismTheme) => void;
resolvedTheme?: ResolvedPrismTheme;
codeBlocks?: NodeListOf<HTMLPreElement>;
- setCodeBlocks: (codeBlocks: NodeListOf<HTMLPreElement>) => void;
};
export type PrismThemeProviderProps = {
attribute?: string;
+ children: ReactNode;
storageKey?: string;
themes?: PrismTheme[];
};
@@ -31,14 +34,16 @@ export const PrismThemeContext = createContext<UsePrismThemeProps>({
setTheme: (_) => {
// This is intentional.
},
- setCodeBlocks: (_) => {
- // This is intentional.
- },
});
export const usePrismTheme = () => useContext(PrismThemeContext);
-const prefersDarkScheme = () => {
+/**
+ * Check if user prefers dark color scheme.
+ *
+ * @returns {boolean|undefined} True if `prefers-color-scheme` is set to `dark`.
+ */
+const prefersDarkScheme = (): boolean | undefined => {
if (typeof window === 'undefined') return;
return (
@@ -47,40 +52,35 @@ const prefersDarkScheme = () => {
);
};
+/**
+ * Check if a given string is a Prism theme name.
+ *
+ * @param {string} theme - A string.
+ * @returns {boolean} True if the given string match a Prism theme name.
+ */
const isValidTheme = (theme: string): boolean => {
return theme === 'dark' || theme === 'light' || theme === 'system';
};
-const getTheme = (key: string): PrismTheme | undefined => {
- if (typeof window === 'undefined') return undefined;
- const storageValue = LocalStorage.get(key);
-
- return storageValue && isValidTheme(storageValue)
- ? (storageValue as PrismTheme)
- : undefined;
-};
-
export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({
attribute = 'data-prismjs-color-scheme-current',
storageKey = 'prismjs-color-scheme',
themes = ['dark', 'light', 'system'],
children,
}) => {
+ /**
+ * Retrieve the theme to use depending on `prefers-color-scheme`.
+ */
const getThemeFromSystem = useCallback(() => {
return prefersDarkScheme() ? 'dark' : 'light';
}, []);
- const [prismTheme, setPrismTheme] = useState<PrismTheme>(
- getTheme(storageKey) || 'system'
- );
-
- const updateTheme = (theme: PrismTheme) => {
- setPrismTheme(theme);
- };
+ const { value: prismTheme, setValue: setPrismTheme } =
+ useLocalStorage<PrismTheme>(storageKey, 'system');
useEffect(() => {
- LocalStorage.set(storageKey, prismTheme);
- }, [prismTheme, storageKey]);
+ if (!isValidTheme(prismTheme)) setPrismTheme('system');
+ }, [prismTheme, setPrismTheme]);
const [resolvedTheme, setResolvedTheme] = useState<ResolvedPrismTheme>();
@@ -107,22 +107,12 @@ export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({
.removeEventListener('change', updateResolvedTheme);
}, [updateResolvedTheme]);
- const [preTags, setPreTags] = useState<NodeListOf<HTMLPreElement>>();
-
- const updatePreTags = useCallback((tags: NodeListOf<HTMLPreElement>) => {
- setPreTags(tags);
- }, []);
-
- const updatePreTagsAttribute = useCallback(() => {
- preTags?.forEach((pre) => {
- pre.setAttribute(attribute, prismTheme);
- });
- }, [attribute, preTags, prismTheme]);
-
- useEffect(() => {
- updatePreTagsAttribute();
- }, [updatePreTagsAttribute, prismTheme]);
+ const preTags = useQuerySelectorAll<'pre'>('pre');
+ useAttributes({ elements: preTags, attribute, value: prismTheme });
+ /**
+ * Listen for changes on pre attributes and update theme.
+ */
const listenAttributeChange = useCallback(
(pre: HTMLPreElement) => {
var observer = new MutationObserver(function (mutations) {
@@ -137,15 +127,12 @@ export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({
attributeFilter: [attribute],
});
},
- [attribute]
+ [attribute, setPrismTheme]
);
useEffect(() => {
if (!preTags) return;
-
- preTags.forEach((pre) => {
- listenAttributeChange(pre);
- });
+ preTags.forEach(listenAttributeChange);
}, [preTags, listenAttributeChange]);
return (
@@ -153,9 +140,8 @@ export const PrismThemeProvider: FC<PrismThemeProviderProps> = ({
value={{
themes,
theme: prismTheme,
- setTheme: updateTheme,
+ setTheme: setPrismTheme,
codeBlocks: preTags,
- setCodeBlocks: updatePreTags,
resolvedTheme,
}}
>
diff --git a/yarn.lock b/yarn.lock
index d936c98..3388622 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,7 +9,7 @@
dependencies:
"@jridgewell/trace-mapping" "^0.3.0"
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7":
+"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==
@@ -21,6 +21,28 @@
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.7.tgz#078d8b833fbbcc95286613be8c716cef2b519fa2"
integrity sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==
+"@babel/core@7.12.9":
+ version "7.12.9"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8"
+ integrity sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/generator" "^7.12.5"
+ "@babel/helper-module-transforms" "^7.12.1"
+ "@babel/helpers" "^7.12.5"
+ "@babel/parser" "^7.12.7"
+ "@babel/template" "^7.12.7"
+ "@babel/traverse" "^7.12.9"
+ "@babel/types" "^7.12.7"
+ convert-source-map "^1.7.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.1"
+ json5 "^2.1.2"
+ lodash "^4.17.19"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.15.5", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe"
@@ -42,7 +64,37 @@
json5 "^2.2.1"
semver "^6.3.0"
-"@babel/generator@^7.17.9", "@babel/generator@^7.7.2":
+"@babel/core@^7.12.10", "@babel/core@^7.17.8", "@babel/core@^7.7.5":
+ version "7.17.8"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.8.tgz#3dac27c190ebc3a4381110d46c80e77efe172e1a"
+ integrity sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==
+ dependencies:
+ "@ampproject/remapping" "^2.1.0"
+ "@babel/code-frame" "^7.16.7"
+ "@babel/generator" "^7.17.7"
+ "@babel/helper-compilation-targets" "^7.17.7"
+ "@babel/helper-module-transforms" "^7.17.7"
+ "@babel/helpers" "^7.17.8"
+ "@babel/parser" "^7.17.8"
+ "@babel/template" "^7.16.7"
+ "@babel/traverse" "^7.17.3"
+ "@babel/types" "^7.17.0"
+ convert-source-map "^1.7.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.1.2"
+ semver "^6.3.0"
+
+"@babel/generator@^7.12.11", "@babel/generator@^7.12.5":
+ version "7.17.7"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.7.tgz#8da2599beb4a86194a3b24df6c085931d9ee45ad"
+ integrity sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==
+ dependencies:
+ "@babel/types" "^7.17.0"
+ jsesc "^2.5.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.17.3", "@babel/generator@^7.17.7", "@babel/generator@^7.17.9", "@babel/generator@^7.7.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.9.tgz#f4af9fd38fa8de143c29fce3f71852406fc1e2fc"
integrity sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==
@@ -89,6 +141,19 @@
"@babel/helper-replace-supers" "^7.16.7"
"@babel/helper-split-export-declaration" "^7.16.7"
+"@babel/helper-create-class-features-plugin@^7.17.12":
+ version "7.18.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.0.tgz#fac430912606331cb075ea8d82f9a4c145a4da19"
+ integrity sha512-Kh8zTGR9de3J63e5nS0rQUdRs/kbtwoeQQ0sriS0lItjC96u8XXZN6lKpuyWd2coKSU13py/y+LTmThLuVX0Pg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.16.7"
+ "@babel/helper-environment-visitor" "^7.16.7"
+ "@babel/helper-function-name" "^7.17.9"
+ "@babel/helper-member-expression-to-functions" "^7.17.7"
+ "@babel/helper-optimise-call-expression" "^7.16.7"
+ "@babel/helper-replace-supers" "^7.16.7"
+ "@babel/helper-split-export-declaration" "^7.16.7"
+
"@babel/helper-create-regexp-features-plugin@^7.16.7":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1"
@@ -97,6 +162,20 @@
"@babel/helper-annotate-as-pure" "^7.16.7"
regexpu-core "^5.0.1"
+"@babel/helper-define-polyfill-provider@^0.1.5":
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.5.tgz#3c2f91b7971b9fc11fe779c945c014065dea340e"
+ integrity sha512-nXuzCSwlJ/WKr8qxzW816gwyT6VZgiJG17zR40fou70yfAcqjoNyTLl/DQ+FExw5Hx5KNqshmN8Ldl/r2N7cTg==
+ dependencies:
+ "@babel/helper-compilation-targets" "^7.13.0"
+ "@babel/helper-module-imports" "^7.12.13"
+ "@babel/helper-plugin-utils" "^7.13.0"
+ "@babel/traverse" "^7.13.0"
+ debug "^4.1.1"
+ lodash.debounce "^4.0.8"
+ resolve "^1.14.2"
+ semver "^6.1.2"
+
"@babel/helper-define-polyfill-provider@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665"
@@ -147,14 +226,14 @@
dependencies:
"@babel/types" "^7.17.0"
-"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7":
+"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==
dependencies:
"@babel/types" "^7.16.7"
-"@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7":
+"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.17.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz#3943c7f777139e7954a5355c815263741a9c1cbd"
integrity sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==
@@ -175,11 +254,21 @@
dependencies:
"@babel/types" "^7.16.7"
+"@babel/helper-plugin-utils@7.10.4":
+ version "7.10.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375"
+ integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==
+
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5"
integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==
+"@babel/helper-plugin-utils@^7.17.12":
+ version "7.17.12"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96"
+ integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==
+
"@babel/helper-remap-async-to-generator@^7.16.8":
version "7.16.8"
resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3"
@@ -241,7 +330,16 @@
"@babel/traverse" "^7.16.8"
"@babel/types" "^7.16.8"
-"@babel/helpers@^7.17.9":
+"@babel/helpers@^7.12.5":
+ version "7.17.8"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.8.tgz#288450be8c6ac7e4e44df37bcc53d345e07bc106"
+ integrity sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==
+ dependencies:
+ "@babel/template" "^7.16.7"
+ "@babel/traverse" "^7.17.3"
+ "@babel/types" "^7.17.0"
+
+"@babel/helpers@^7.17.8", "@babel/helpers@^7.17.9":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.9.tgz#b2af120821bfbe44f9907b1826e168e819375a1a"
integrity sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==
@@ -259,11 +357,16 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.4", "@babel/parser@^7.16.7", "@babel/parser@^7.17.9":
+"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.4", "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8", "@babel/parser@^7.17.9":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.9.tgz#9c94189a6062f0291418ca021077983058e171ef"
integrity sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==
+"@babel/parser@^7.12.11", "@babel/parser@^7.12.7":
+ version "7.17.8"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240"
+ integrity sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==
+
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050"
@@ -289,7 +392,7 @@
"@babel/helper-remap-async-to-generator" "^7.16.8"
"@babel/plugin-syntax-async-generators" "^7.8.4"
-"@babel/plugin-proposal-class-properties@^7.16.7":
+"@babel/plugin-proposal-class-properties@^7.12.1", "@babel/plugin-proposal-class-properties@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0"
integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==
@@ -306,6 +409,17 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
+"@babel/plugin-proposal-decorators@^7.12.12":
+ version "7.17.8"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.17.8.tgz#4f0444e896bee85d35cf714a006fc5418f87ff00"
+ integrity sha512-U69odN4Umyyx1xO1rTII0IDkAEC+RNlcKXtqOblfpzqy1C+aOplb76BQNq0+XdpVkOaPlpEDwd++joY8FNFJKA==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.17.6"
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/helper-replace-supers" "^7.16.7"
+ "@babel/plugin-syntax-decorators" "^7.17.0"
+ charcodes "^0.2.0"
+
"@babel/plugin-proposal-dynamic-import@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2"
@@ -314,6 +428,14 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-dynamic-import" "^7.8.3"
+"@babel/plugin-proposal-export-default-from@^7.12.1":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.16.7.tgz#a40ab158ca55627b71c5513f03d3469026a9e929"
+ integrity sha512-+cENpW1rgIjExn+o5c8Jw/4BuH4eGKKYvkMB8/0ZxFQ9mC0t4z09VsPIwNg6waF69QYC81zxGeAsREGuqQoKeg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-export-default-from" "^7.16.7"
+
"@babel/plugin-proposal-export-namespace-from@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163"
@@ -338,7 +460,7 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
-"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7":
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99"
integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==
@@ -354,7 +476,16 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-numeric-separator" "^7.10.4"
-"@babel/plugin-proposal-object-rest-spread@^7.16.7":
+"@babel/plugin-proposal-object-rest-spread@7.12.1":
+ version "7.12.1"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz#def9bd03cea0f9b72283dac0ec22d289c7691069"
+ integrity sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+ "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+ "@babel/plugin-transform-parameters" "^7.12.1"
+
+"@babel/plugin-proposal-object-rest-spread@^7.12.1", "@babel/plugin-proposal-object-rest-spread@^7.16.7":
version "7.17.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz#d9eb649a54628a51701aef7e0ea3d17e2b9dd390"
integrity sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==
@@ -373,7 +504,7 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
-"@babel/plugin-proposal-optional-chaining@^7.16.7":
+"@babel/plugin-proposal-optional-chaining@^7.12.7", "@babel/plugin-proposal-optional-chaining@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a"
integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==
@@ -382,7 +513,7 @@
"@babel/helper-skip-transparent-expression-wrappers" "^7.16.0"
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
-"@babel/plugin-proposal-private-methods@^7.16.11":
+"@babel/plugin-proposal-private-methods@^7.12.1", "@babel/plugin-proposal-private-methods@^7.16.11":
version "7.16.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz#e8df108288555ff259f4527dbe84813aac3a1c50"
integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==
@@ -390,6 +521,16 @@
"@babel/helper-create-class-features-plugin" "^7.16.10"
"@babel/helper-plugin-utils" "^7.16.7"
+"@babel/plugin-proposal-private-property-in-object@^7.12.1":
+ version "7.17.12"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.17.12.tgz#b02efb7f106d544667d91ae97405a9fd8c93952d"
+ integrity sha512-/6BtVi57CJfrtDNKfK5b66ydK2J5pXUKBKSPD2G1whamMuEnZWgoOIfO8Vf9F/DoD4izBLD/Au4NMQfruzzykg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.16.7"
+ "@babel/helper-create-class-features-plugin" "^7.17.12"
+ "@babel/helper-plugin-utils" "^7.17.12"
+ "@babel/plugin-syntax-private-property-in-object" "^7.14.5"
+
"@babel/plugin-proposal-private-property-in-object@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce"
@@ -436,6 +577,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.14.5"
+"@babel/plugin-syntax-decorators@^7.17.0":
+ version "7.17.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.0.tgz#a2be3b2c9fe7d78bd4994e790896bc411e2f166d"
+ integrity sha512-qWe85yCXsvDEluNP0OyeQjH63DlhAR3W7K9BxxU1MvbDb48tgBG+Ao6IJJ6smPDrrVzSQZrbF6donpkFBMcs3A==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+
"@babel/plugin-syntax-dynamic-import@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
@@ -443,6 +591,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
+"@babel/plugin-syntax-export-default-from@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.16.7.tgz#fa89cf13b60de2c3f79acdc2b52a21174c6de060"
+ integrity sha512-4C3E4NsrLOgftKaTYTULhHsuQrGv3FHrBzOMDiS7UYKIpgGBkAdawg4h+EI8zPeK9M0fiIIh72hIwsI24K7MbA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+
"@babel/plugin-syntax-export-namespace-from@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a"
@@ -450,6 +605,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.3"
+"@babel/plugin-syntax-flow@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz#202b147e5892b8452bbb0bb269c7ed2539ab8832"
+ integrity sha512-UDo3YGQO0jH6ytzVwgSLv9i/CzMcUjbKenL67dTrAZPPv6GFAtDhe6jqnvmoKzC/7htNTohhos+onPtDMqJwaQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+
"@babel/plugin-syntax-import-meta@^7.8.3":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51"
@@ -464,6 +626,13 @@
dependencies:
"@babel/helper-plugin-utils" "^7.8.0"
+"@babel/plugin-syntax-jsx@7.12.1":
+ version "7.12.1"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz#9d9d357cc818aa7ae7935917c1257f67677a0926"
+ integrity sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.10.4"
+
"@babel/plugin-syntax-jsx@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz#50b6571d13f764266a113d77c82b4a6508bbe665"
@@ -492,7 +661,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
-"@babel/plugin-syntax-object-rest-spread@^7.8.3":
+"@babel/plugin-syntax-object-rest-spread@7.8.3", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
version "7.8.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
@@ -534,7 +703,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-arrow-functions@^7.16.7":
+"@babel/plugin-transform-arrow-functions@^7.12.1", "@babel/plugin-transform-arrow-functions@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154"
integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==
@@ -557,14 +726,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-block-scoping@^7.16.7":
+"@babel/plugin-transform-block-scoping@^7.12.12", "@babel/plugin-transform-block-scoping@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87"
integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-classes@^7.16.7":
+"@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00"
integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==
@@ -585,7 +754,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-destructuring@^7.16.7":
+"@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.16.7":
version "7.17.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz#49dc2675a7afa9a5e4c6bdee636061136c3408d1"
integrity sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==
@@ -615,7 +784,15 @@
"@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-for-of@^7.16.7":
+"@babel/plugin-transform-flow-strip-types@^7.16.7":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz#291fb140c78dabbf87f2427e7c7c332b126964b8"
+ integrity sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/plugin-syntax-flow" "^7.16.7"
+
+"@babel/plugin-transform-for-of@^7.12.1", "@babel/plugin-transform-for-of@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c"
integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==
@@ -705,7 +882,7 @@
"@babel/helper-plugin-utils" "^7.16.7"
"@babel/helper-replace-supers" "^7.16.7"
-"@babel/plugin-transform-parameters@^7.16.7":
+"@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f"
integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==
@@ -740,7 +917,7 @@
dependencies:
"@babel/plugin-transform-react-jsx" "^7.16.7"
-"@babel/plugin-transform-react-jsx@^7.16.7":
+"@babel/plugin-transform-react-jsx@^7.12.12", "@babel/plugin-transform-react-jsx@^7.16.7":
version "7.17.3"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.3.tgz#eac1565da176ccb1a715dae0b4609858808008c1"
integrity sha512-9tjBm4O07f7mzKSIlEmPdiE6ub7kfIe6Cd+w+oQebpATfTQMAgW+YOuWxogbKVTulA+MEO7byMeIUtQ1z+z+ZQ==
@@ -773,14 +950,14 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-shorthand-properties@^7.16.7":
+"@babel/plugin-transform-shorthand-properties@^7.12.1", "@babel/plugin-transform-shorthand-properties@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a"
integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-spread@^7.16.7":
+"@babel/plugin-transform-spread@^7.12.1", "@babel/plugin-transform-spread@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44"
integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==
@@ -795,7 +972,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/plugin-transform-template-literals@^7.16.7":
+"@babel/plugin-transform-template-literals@^7.12.1", "@babel/plugin-transform-template-literals@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab"
integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==
@@ -833,7 +1010,7 @@
"@babel/helper-create-regexp-features-plugin" "^7.16.7"
"@babel/helper-plugin-utils" "^7.16.7"
-"@babel/preset-env@^7.15.6":
+"@babel/preset-env@^7.12.11", "@babel/preset-env@^7.15.6":
version "7.16.11"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982"
integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g==
@@ -913,6 +1090,15 @@
core-js-compat "^3.20.2"
semver "^6.3.0"
+"@babel/preset-flow@^7.12.1":
+ version "7.16.7"
+ resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.16.7.tgz#7fd831323ab25eeba6e4b77a589f680e30581cbd"
+ integrity sha512-6ceP7IyZdUYQ3wUVqyRSQXztd1YmFHWI4Xv11MIqAlE4WqxBSd/FZ61V9k+TS5Gd4mkHOtQtPp9ymRpxH4y1Ug==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.16.7"
+ "@babel/helper-validator-option" "^7.16.7"
+ "@babel/plugin-transform-flow-strip-types" "^7.16.7"
+
"@babel/preset-modules@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9"
@@ -924,7 +1110,7 @@
"@babel/types" "^7.4.4"
esutils "^2.0.2"
-"@babel/preset-react@^7.14.5":
+"@babel/preset-react@^7.12.10", "@babel/preset-react@^7.14.5":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.16.7.tgz#4c18150491edc69c183ff818f9f2aecbe5d93852"
integrity sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA==
@@ -936,7 +1122,7 @@
"@babel/plugin-transform-react-jsx-development" "^7.16.7"
"@babel/plugin-transform-react-pure-annotations" "^7.16.7"
-"@babel/preset-typescript@^7.15.0":
+"@babel/preset-typescript@^7.12.7", "@babel/preset-typescript@^7.15.0":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz#ab114d68bb2020afc069cd51b37ff98a046a70b9"
integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ==
@@ -945,6 +1131,17 @@
"@babel/helper-validator-option" "^7.16.7"
"@babel/plugin-transform-typescript" "^7.16.7"
+"@babel/register@^7.12.1":
+ version "7.17.7"
+ resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.7.tgz#5eef3e0f4afc07e25e847720e7b987ae33f08d0b"
+ integrity sha512-fg56SwvXRifootQEDQAu1mKdjh5uthPzdO0N6t358FktfL4XjAVXuH58ULoiW8mesxiOgNIrxiImqEwv0+hRRA==
+ dependencies:
+ clone-deep "^4.0.1"
+ find-cache-dir "^2.0.0"
+ make-dir "^2.1.0"
+ pirates "^4.0.5"
+ source-map-support "^0.5.16"
+
"@babel/runtime-corejs3@^7.10.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.17.9.tgz#3d02d0161f0fbf3ada8e88159375af97690f4055"
@@ -953,6 +1150,20 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
+"@babel/runtime@7.7.2":
+ version "7.7.2"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a"
+ integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==
+ dependencies:
+ regenerator-runtime "^0.13.2"
+
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.16.7", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6":
+ version "7.17.8"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2"
+ integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
@@ -960,7 +1171,21 @@
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/template@^7.16.7", "@babel/template@^7.3.3":
+"@babel/runtime@^7.11.2", "@babel/runtime@^7.17.8":
+ version "7.18.0"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.0.tgz#6d77142a19cb6088f0af662af1ada37a604d34ae"
+ integrity sha512-YMQvx/6nKEaucl0MY56mwIG483xk8SDNdlUwb2Ts6FUpr7fm85DxEmsY18LXBNhcTz6tO6JwZV8w1W06v8UKeg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
+"@babel/runtime@~7.5.4":
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
+ integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
+ dependencies:
+ regenerator-runtime "^0.13.2"
+
+"@babel/template@^7.12.7", "@babel/template@^7.16.7", "@babel/template@^7.3.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==
@@ -969,6 +1194,22 @@
"@babel/parser" "^7.16.7"
"@babel/types" "^7.16.7"
+"@babel/traverse@^7.1.6", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9":
+ version "7.17.3"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57"
+ integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==
+ dependencies:
+ "@babel/code-frame" "^7.16.7"
+ "@babel/generator" "^7.17.3"
+ "@babel/helper-environment-visitor" "^7.16.7"
+ "@babel/helper-function-name" "^7.16.7"
+ "@babel/helper-hoist-variables" "^7.16.7"
+ "@babel/helper-split-export-declaration" "^7.16.7"
+ "@babel/parser" "^7.17.3"
+ "@babel/types" "^7.17.0"
+ debug "^4.1.0"
+ globals "^11.1.0"
+
"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.17.9", "@babel/traverse@^7.7.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.9.tgz#1f9b207435d9ae4a8ed6998b2b82300d83c37a0d"
@@ -985,7 +1226,7 @@
debug "^4.1.0"
globals "^11.1.0"
-"@babel/types@^7.0.0", "@babel/types@^7.15.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
+"@babel/types@^7.0.0", "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.15.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b"
integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==
@@ -993,11 +1234,24 @@
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
+"@base2/pretty-print-object@1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4"
+ integrity sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
+"@cnakazawa/watch@^1.0.3":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
+ integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==
+ dependencies:
+ exec-sh "^0.3.2"
+ minimist "^1.2.0"
+
"@commitlint/cli@^16.2.3":
version "16.2.3"
resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-16.2.3.tgz#6c250ce7a660a08a3ac35dd2ec5039421fb831df"
@@ -1167,6 +1421,159 @@
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
+"@design-systems/utils@2.12.0":
+ version "2.12.0"
+ resolved "https://registry.yarnpkg.com/@design-systems/utils/-/utils-2.12.0.tgz#955c108be07cb8f01532207cbfea8f848fa760c9"
+ integrity sha512-Y/d2Zzr+JJfN6u1gbuBUb1ufBuLMJJRZQk+dRmw8GaTpqKx5uf7cGUYGTwN02dIb3I+Tf+cW8jcGBTRiFxdYFg==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ clsx "^1.0.4"
+ focus-lock "^0.8.0"
+ react-merge-refs "^1.0.0"
+
+"@devtools-ds/object-inspector@^1.1.2":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@devtools-ds/object-inspector/-/object-inspector-1.2.0.tgz#64a132fbd4159affa5a87c8cf6cf8540c337aed2"
+ integrity sha512-VztcwqVwScSvYdvJVZBJYsVO/2Pew3JPpFV3T9fuCHQLlHcLYOV3aU/kBS2ScuE2O1JN0ZbobLqFLa3vQF54Fw==
+ dependencies:
+ "@babel/runtime" "7.7.2"
+ "@devtools-ds/object-parser" "^1.2.0"
+ "@devtools-ds/themes" "^1.2.0"
+ "@devtools-ds/tree" "^1.2.0"
+ clsx "1.1.0"
+
+"@devtools-ds/object-parser@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@devtools-ds/object-parser/-/object-parser-1.2.0.tgz#8da39bf481687afdf113c78dbac5ced6fd8e30d1"
+ integrity sha512-SjGGyiFFY8dtUpiWXAvRSzRT+hE11EAAysrq2PsC/GVLf2ZLyT2nHlQO5kDStywyTz+fjw7S7pyDRj1HG9YTTA==
+ dependencies:
+ "@babel/runtime" "~7.5.4"
+
+"@devtools-ds/themes@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@devtools-ds/themes/-/themes-1.2.0.tgz#2fda60af9741e97bc09257b512e49a7aecf6f4bc"
+ integrity sha512-LimEITorE6yWZWWuMc6OiBfLQgPrQqWbyMEmfRUDPa3PHXoAY4SpDxczfg31fgyRDUNWnZhjaJH5bBbu8VEbIw==
+ dependencies:
+ "@babel/runtime" "~7.5.4"
+ "@design-systems/utils" "2.12.0"
+ clsx "1.1.0"
+
+"@devtools-ds/tree@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@devtools-ds/tree/-/tree-1.2.0.tgz#e882d10ae13a30f2aa02e75c3eeb6c44a47a80c3"
+ integrity sha512-hC4g4ocuo2eg7jsnzKdauxH0sDQiPW3KSM2+uK3kRgcmr9PzpBD5Kob+Y/WFSVKswFleftOGKL4BQLuRv0sPxA==
+ dependencies:
+ "@babel/runtime" "7.7.2"
+ "@devtools-ds/themes" "^1.2.0"
+ clsx "1.1.0"
+
+"@discoveryjs/json-ext@^0.5.3":
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
+ integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
+
+"@emotion/cache@^10.0.27":
+ version "10.0.29"
+ resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
+ integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
+ dependencies:
+ "@emotion/sheet" "0.9.4"
+ "@emotion/stylis" "0.8.5"
+ "@emotion/utils" "0.11.3"
+ "@emotion/weak-memoize" "0.2.5"
+
+"@emotion/core@^10.1.1":
+ version "10.3.1"
+ resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.3.1.tgz#4021b6d8b33b3304d48b0bb478485e7d7421c69d"
+ integrity sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ "@emotion/cache" "^10.0.27"
+ "@emotion/css" "^10.0.27"
+ "@emotion/serialize" "^0.11.15"
+ "@emotion/sheet" "0.9.4"
+ "@emotion/utils" "0.11.3"
+
+"@emotion/css@^10.0.27":
+ version "10.0.27"
+ resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c"
+ integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==
+ dependencies:
+ "@emotion/serialize" "^0.11.15"
+ "@emotion/utils" "0.11.3"
+ babel-plugin-emotion "^10.0.27"
+
+"@emotion/hash@0.8.0":
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
+ integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@emotion/is-prop-valid@0.8.8", "@emotion/is-prop-valid@^0.8.6":
+ version "0.8.8"
+ resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
+ integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
+ dependencies:
+ "@emotion/memoize" "0.7.4"
+
+"@emotion/memoize@0.7.4":
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
+ integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
+
+"@emotion/serialize@^0.11.15", "@emotion/serialize@^0.11.16":
+ version "0.11.16"
+ resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-0.11.16.tgz#dee05f9e96ad2fb25a5206b6d759b2d1ed3379ad"
+ integrity sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==
+ dependencies:
+ "@emotion/hash" "0.8.0"
+ "@emotion/memoize" "0.7.4"
+ "@emotion/unitless" "0.7.5"
+ "@emotion/utils" "0.11.3"
+ csstype "^2.5.7"
+
+"@emotion/sheet@0.9.4":
+ version "0.9.4"
+ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-0.9.4.tgz#894374bea39ec30f489bbfc3438192b9774d32e5"
+ integrity sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==
+
+"@emotion/styled-base@^10.3.0":
+ version "10.3.0"
+ resolved "https://registry.yarnpkg.com/@emotion/styled-base/-/styled-base-10.3.0.tgz#9aa2c946100f78b47316e4bc6048321afa6d4e36"
+ integrity sha512-PBRqsVKR7QRNkmfH78hTSSwHWcwDpecH9W6heujWAcyp2wdz/64PP73s7fWS1dIPm8/Exc8JAzYS8dEWXjv60w==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ "@emotion/is-prop-valid" "0.8.8"
+ "@emotion/serialize" "^0.11.15"
+ "@emotion/utils" "0.11.3"
+
+"@emotion/styled@^10.0.27":
+ version "10.3.0"
+ resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-10.3.0.tgz#8ee959bf75730789abb5f67f7c3ded0c30aec876"
+ integrity sha512-GgcUpXBBEU5ido+/p/mCT2/Xx+Oqmp9JzQRuC+a4lYM4i4LBBn/dWvc0rQ19N9ObA8/T4NWMrPNe79kMBDJqoQ==
+ dependencies:
+ "@emotion/styled-base" "^10.3.0"
+ babel-plugin-emotion "^10.0.27"
+
+"@emotion/stylis@0.8.5":
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/@emotion/stylis/-/stylis-0.8.5.tgz#deacb389bd6ee77d1e7fcaccce9e16c5c7e78e04"
+ integrity sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==
+
+"@emotion/unitless@0.7.5":
+ version "0.7.5"
+ resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
+ integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+
+"@emotion/utils@0.11.3":
+ version "0.11.3"
+ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.11.3.tgz#a759863867befa7e583400d322652a3f44820924"
+ integrity sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==
+
+"@emotion/weak-memoize@0.2.5":
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
+ integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
+
"@eslint/eslintrc@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.1.tgz#8b5e1c49f4077235516bc9ec7d41378c0f69b8c6"
@@ -1261,10 +1668,10 @@
dependencies:
tslib "^2.1.0"
-"@formatjs/intl@2.1.1":
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.1.1.tgz#288f130a15b85ec1f4d022a379a5c88b27767bcb"
- integrity sha512-iUjBnV2XE+mS3run+Rj/96rfxvwSiCsqMrSbIWoU4dOjIYil7boZK2mCamxoz8CqiiL4VD4ym5EEDbYPWirlFA==
+"@formatjs/intl@2.2.0":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@formatjs/intl/-/intl-2.2.0.tgz#3714952ea33f5f23a416ed8d1ca369f3cb18dddd"
+ integrity sha512-2wLE64ns7QcQvqeNmwIFZBnDZ6XcL1QKx+x7A9J25XykCIgIMZNF5Dx7LAxJplCLRCqh2Eh0Q4ZVC1GLcZA4MQ==
dependencies:
"@formatjs/ecma402-abstract" "1.11.4"
"@formatjs/fast-memoize" "1.2.1"
@@ -1296,6 +1703,11 @@
tslib "^2.1.0"
typescript "^4.5"
+"@gar/promisify@^1.0.1":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
+ integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
+
"@humanwhocodes/config-array@^0.9.2":
version "0.9.5"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
@@ -1468,6 +1880,27 @@
jest-haste-map "^27.5.1"
jest-runtime "^27.5.1"
+"@jest/transform@^26.6.2":
+ version "26.6.2"
+ resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b"
+ integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==
+ dependencies:
+ "@babel/core" "^7.1.0"
+ "@jest/types" "^26.6.2"
+ babel-plugin-istanbul "^6.0.0"
+ chalk "^4.0.0"
+ convert-source-map "^1.4.0"
+ fast-json-stable-stringify "^2.0.0"
+ graceful-fs "^4.2.4"
+ jest-haste-map "^26.6.2"
+ jest-regex-util "^26.0.0"
+ jest-util "^26.6.2"
+ micromatch "^4.0.2"
+ pirates "^4.0.1"
+ slash "^3.0.0"
+ source-map "^0.6.1"
+ write-file-atomic "^3.0.0"
+
"@jest/transform@^27.5.1":
version "27.5.1"
resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-27.5.1.tgz#6c3501dcc00c4c08915f292a600ece5ecfe1f409"
@@ -1489,6 +1922,17 @@
source-map "^0.6.1"
write-file-atomic "^3.0.0"
+"@jest/types@^26.6.2":
+ version "26.6.2"
+ resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e"
+ integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==
+ dependencies:
+ "@types/istanbul-lib-coverage" "^2.0.0"
+ "@types/istanbul-reports" "^3.0.0"
+ "@types/node" "*"
+ "@types/yargs" "^15.0.0"
+ chalk "^4.0.0"
+
"@jest/types@^27.5.1":
version "27.5.1"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80"
@@ -1526,6 +1970,31 @@
"@mdx-js/mdx" "^2.0.0"
source-map "^0.7.0"
+"@mdx-js/mdx@^1.6.22":
+ version "1.6.22"
+ resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
+ integrity sha512-AMxuLxPz2j5/6TpF/XSdKpQP1NlG0z11dFOlq+2IP/lSgl11GY8ji6S/rgsViN/L0BDvHvUMruRb7ub+24LUYA==
+ dependencies:
+ "@babel/core" "7.12.9"
+ "@babel/plugin-syntax-jsx" "7.12.1"
+ "@babel/plugin-syntax-object-rest-spread" "7.8.3"
+ "@mdx-js/util" "1.6.22"
+ babel-plugin-apply-mdx-type-prop "1.6.22"
+ babel-plugin-extract-import-names "1.6.22"
+ camelcase-css "2.0.1"
+ detab "2.0.4"
+ hast-util-raw "6.0.1"
+ lodash.uniq "4.5.0"
+ mdast-util-to-hast "10.0.1"
+ remark-footnotes "2.0.0"
+ remark-mdx "1.6.22"
+ remark-parse "8.0.3"
+ remark-squeeze-paragraphs "4.0.0"
+ style-to-object "0.3.0"
+ unified "9.2.0"
+ unist-builder "2.0.3"
+ unist-util-visit "2.0.3"
+
"@mdx-js/mdx@^2.0.0":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-2.1.1.tgz#6d8b9b75456d7685a52c3812b1c3e4830c7458fb"
@@ -1549,6 +2018,11 @@
unist-util-visit "^4.0.0"
vfile "^5.0.0"
+"@mdx-js/react@^1.6.22":
+ version "1.6.22"
+ resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-1.6.22.tgz#ae09b4744fddc74714ee9f9d6f17a66e77c43573"
+ integrity sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==
+
"@mdx-js/react@^2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-2.1.1.tgz#c59d844fd61b776fea8673fb77405d4e14db48c5"
@@ -1557,6 +2031,19 @@
"@types/mdx" "^2.0.0"
"@types/react" ">=16"
+"@mdx-js/util@1.6.22":
+ version "1.6.22"
+ resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b"
+ integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==
+
+"@mrmlnc/readdir-enhanced@^2.2.1":
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
+ integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==
+ dependencies:
+ call-me-maybe "^1.0.1"
+ glob-to-regexp "^0.3.0"
+
"@next/bundle-analyzer@^12.1.5":
version "12.1.5"
resolved "https://registry.yarnpkg.com/@next/bundle-analyzer/-/bundle-analyzer-12.1.5.tgz#07079b892efe0a2a7e8add703ad7cacfa3cc4e88"
@@ -1654,6 +2141,11 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+"@nodelib/fs.stat@^1.1.2":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
+ integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
+
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
@@ -1662,6 +2154,37 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@npmcli/fs@^1.0.0":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257"
+ integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==
+ dependencies:
+ "@gar/promisify" "^1.0.1"
+ semver "^7.3.5"
+
+"@npmcli/move-file@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
+ integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==
+ dependencies:
+ mkdirp "^1.0.4"
+ rimraf "^3.0.2"
+
+"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.6.tgz#9ced74cb23dae31ab385f775e237ce4c50422a1d"
+ integrity sha512-IIWxofIYt/AbMwoeBgj+O2aAXLrlCQVg+A4a2zfpXFNHgP8o8rvi3v+oe5t787Lj+KXlKOh8BAiUp9bhuELXhg==
+ dependencies:
+ ansi-html-community "^0.0.8"
+ common-path-prefix "^3.0.0"
+ core-js-pure "^3.8.1"
+ error-stack-parser "^2.0.6"
+ find-up "^5.0.0"
+ html-entities "^2.1.0"
+ loader-utils "^2.0.0"
+ schema-utils "^3.0.0"
+ source-map "^0.7.3"
+
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
@@ -1686,6 +2209,1063 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
+"@storybook/addon-actions@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-6.5.3.tgz#1e47bcfc5b215f483aef9d7f22e3619cd354af8c"
+ integrity sha512-OYfG6dDFoNIPmtQ5vXum7m7U5MDg6rlwkaUpV3MkMVCnSAco0/GGRdsYBVO+fpfFFVxRUi3QFEv7xI0xyX2oiQ==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ polished "^4.2.2"
+ prop-types "^15.7.2"
+ react-inspector "^5.1.0"
+ regenerator-runtime "^0.13.7"
+ telejson "^6.0.8"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+ uuid-browser "^3.1.0"
+
+"@storybook/addon-backgrounds@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-6.5.3.tgz#dbe357d88c2844aef5c3c85e055c1c954721a109"
+ integrity sha512-U8ic8fR7kACRvvSaElFaCOgb8ugU2NCtpv2CC3VzxCVWDCdaYivgpXQrvHe0GLYhIrySqgm1wP7q73oILl+Qcg==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ memoizerific "^1.11.3"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/addon-controls@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-6.5.3.tgz#0f398b1c1898bf735bbd228f04a0ddfe1c281807"
+ integrity sha512-90dTS2ySo/u8t/UTY1snsfAJCszvJKW8FNbzxF1c+JwvErb6hrHq0JOSmFLOeRqPuvkKKB8q83vp6A6fXOU+gQ==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/store" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ lodash "^4.17.21"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-docs@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-6.5.3.tgz#6db38b7721245193d2ceaf788844f5a110d1e8bc"
+ integrity sha512-MC1Bwamw8lQvRMmGKsf4UcyNdQCYgpAB2o9m4R0EPA5byTkcEfAXkAwSP8atlP0/wQTjrwvyVgQuhchHmxnR0Q==
+ dependencies:
+ "@babel/plugin-transform-react-jsx" "^7.12.12"
+ "@babel/preset-env" "^7.12.11"
+ "@jest/transform" "^26.6.2"
+ "@mdx-js/react" "^1.6.22"
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/docs-tools" "6.5.3"
+ "@storybook/mdx1-csf" canary
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/postinstall" "6.5.3"
+ "@storybook/preview-web" "6.5.3"
+ "@storybook/source-loader" "6.5.3"
+ "@storybook/store" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ babel-loader "^8.0.0"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ regenerator-runtime "^0.13.7"
+ remark-external-links "^8.0.0"
+ remark-slug "^6.0.0"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/addon-essentials@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-6.5.3.tgz#1bd33d5265288d24049e5fd63e78a99d67fdbe50"
+ integrity sha512-ZVps1kFMB4OuKRS9vIo8u07u04zvU84jP4B7c5TcH/WxFbwPW9I9ePBcCv+QmkdhDTb3TGWRWQqv5zs4cnQ1YA==
+ dependencies:
+ "@storybook/addon-actions" "6.5.3"
+ "@storybook/addon-backgrounds" "6.5.3"
+ "@storybook/addon-controls" "6.5.3"
+ "@storybook/addon-docs" "6.5.3"
+ "@storybook/addon-measure" "6.5.3"
+ "@storybook/addon-outline" "6.5.3"
+ "@storybook/addon-toolbars" "6.5.3"
+ "@storybook/addon-viewport" "6.5.3"
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/node-logger" "6.5.3"
+ core-js "^3.8.2"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-interactions@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-interactions/-/addon-interactions-6.5.3.tgz#5b1cf338df28130286590d5ee05dc22f687024bf"
+ integrity sha512-Jm0QDiNnjo2nvJG/hWlQ2MAINIMo7dzu4b/UsIEWoYBD0z+w5TAN4QEk7TcCg6rYtGFaKZwRnehOvs75Q/mQiQ==
+ dependencies:
+ "@devtools-ds/object-inspector" "^1.1.2"
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/instrumenter" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ jest-mock "^27.0.6"
+ polished "^4.2.2"
+ ts-dedent "^2.2.0"
+
+"@storybook/addon-links@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-6.5.3.tgz#5eae9855e19cc03548aa6655756886c26557a175"
+ integrity sha512-FOcrHbEi8Bw6QE3hzO0vtnJVFj2iIiQk0thEdkhq+Pnn5iZ6VmDoctHPwc2wuyM7BIKDlROaK0VRXYLCx6uiBg==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/router" "6.5.3"
+ "@types/qs" "^6.9.5"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ prop-types "^15.7.2"
+ qs "^6.10.0"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-measure@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-6.5.3.tgz#7a27082d7863981924362fd15022521b0722048f"
+ integrity sha512-8auVdpM66+qaam3KGmfZZgSQ/jJIm6aMeEzi+HX48b2xVa4vv9W9/ZpJp9fc3K2349+BR5K3nzLMObdFr1Yjew==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ core-js "^3.8.2"
+ global "^4.4.0"
+
+"@storybook/addon-outline@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-6.5.3.tgz#2560912eddda92dc827448268176e9b3ac1ef308"
+ integrity sha512-QVSsTOs813Tl404IcWTxdzM+gAIiq46LuH11Re1cMwKrnuOvfHtbLQ4x2n1aTy+mTuWxNymaHwX2Aqilr0fqJA==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+
+"@storybook/addon-toolbars@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-6.5.3.tgz#1a379bae5217ddbcbf37948867211c755d264c70"
+ integrity sha512-wQxDUQASrpdGJouo6WsC840JwaAQkgV4nuCmuxyqbL6yJ7HNyS7mbBoJzfe8kXWOzSN2MM1lP4gOlZuGH4m05g==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/addon-viewport@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-6.5.3.tgz#759d0f5e276a77e032bdcd111162d1944f875a81"
+ integrity sha512-jzHGACC40g/jWXmDIdMyQk5EepNoHhOLFWxUpt12kSgkx0s8PL6PHAOn7p0Yh8JOp0hwHxZJXnYch6yEEgpBTA==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ memoizerific "^1.11.3"
+ prop-types "^15.7.2"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/addons@6.4.19", "@storybook/addons@^6.4.10":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.4.19.tgz#797d912b8b5a86cd6e0d31fa4c42d1f80808a432"
+ integrity sha512-QNyRYhpqmHV8oJxxTBdkRlLSbDFhpBvfvMfIrIT1UXb/eemdBZTaCGVvXZ9UixoEEI7f8VwAQ44IvkU5B1509w==
+ dependencies:
+ "@storybook/api" "6.4.19"
+ "@storybook/channels" "6.4.19"
+ "@storybook/client-logger" "6.4.19"
+ "@storybook/core-events" "6.4.19"
+ "@storybook/csf" "0.0.2--canary.87bc651.0"
+ "@storybook/router" "6.4.19"
+ "@storybook/theming" "6.4.19"
+ "@types/webpack-env" "^1.16.0"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/addons@6.5.3", "@storybook/addons@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/addons/-/addons-6.5.3.tgz#43dd276417edf74be0c465b968d35795f8cd994d"
+ integrity sha512-gzzkxZ7R4+EaEzIEBbTWmkA55JDEDQrDjg3nNY/SJklnRigYdStz41KSPx6HGkF2CaI5BYVd5vZCawYvG16gyg==
+ dependencies:
+ "@storybook/api" "6.5.3"
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/router" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ "@types/webpack-env" "^1.16.0"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/api@6.4.19":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.4.19.tgz#8000a0e4c52c39b910b4ccc6731419e8e71800ef"
+ integrity sha512-aDvea+NpQCBjpNp9YidO1Pr7fzzCp15FSdkG+2ihGQfv5raxrN+IIJnGUXecpe71nvlYiB+29UXBVK7AL0j51Q==
+ dependencies:
+ "@storybook/channels" "6.4.19"
+ "@storybook/client-logger" "6.4.19"
+ "@storybook/core-events" "6.4.19"
+ "@storybook/csf" "0.0.2--canary.87bc651.0"
+ "@storybook/router" "6.4.19"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/theming" "6.4.19"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+ regenerator-runtime "^0.13.7"
+ store2 "^2.12.0"
+ telejson "^5.3.2"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/api@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/api/-/api-6.5.3.tgz#f4c468f348bf6ac65dc571f3e65d67302323312c"
+ integrity sha512-neVW47ssdG3MqwNwTLjlifS/u6zGUkkcK7G/PC1tnQPP9Zc97BStIqS1RnPGie1iawIAT5ZJQefPGJMneSTBKA==
+ dependencies:
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/router" "6.5.3"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+ regenerator-runtime "^0.13.7"
+ store2 "^2.12.0"
+ telejson "^6.0.8"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/builder-webpack4@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/builder-webpack4/-/builder-webpack4-6.5.3.tgz#5e99a009c0132380620a1d177a69a5f352ef6d29"
+ integrity sha512-zhZ879FH8XDs8TRkXN29pGMR2rJrKQYdRn19XTsBt9MlRI8ALFClGixYBsUF/Fa74LAWF2roL5dSt7qDyBQULQ==
+ dependencies:
+ "@babel/core" "^7.12.10"
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/channel-postmessage" "6.5.3"
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/preview-web" "6.5.3"
+ "@storybook/router" "6.5.3"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/store" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ "@storybook/ui" "6.5.3"
+ "@types/node" "^14.0.10 || ^16.0.0"
+ "@types/webpack" "^4.41.26"
+ autoprefixer "^9.8.6"
+ babel-loader "^8.0.0"
+ case-sensitive-paths-webpack-plugin "^2.3.0"
+ core-js "^3.8.2"
+ css-loader "^3.6.0"
+ file-loader "^6.2.0"
+ find-up "^5.0.0"
+ fork-ts-checker-webpack-plugin "^4.1.6"
+ glob "^7.1.6"
+ glob-promise "^3.4.0"
+ global "^4.4.0"
+ html-webpack-plugin "^4.0.0"
+ pnp-webpack-plugin "1.6.4"
+ postcss "^7.0.36"
+ postcss-flexbugs-fixes "^4.2.1"
+ postcss-loader "^4.2.0"
+ raw-loader "^4.0.2"
+ stable "^0.1.8"
+ style-loader "^1.3.0"
+ terser-webpack-plugin "^4.2.3"
+ ts-dedent "^2.0.0"
+ url-loader "^4.1.1"
+ util-deprecate "^1.0.2"
+ webpack "4"
+ webpack-dev-middleware "^3.7.3"
+ webpack-filter-warnings-plugin "^1.2.1"
+ webpack-hot-middleware "^2.25.1"
+ webpack-virtual-modules "^0.2.2"
+
+"@storybook/builder-webpack5@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-6.5.3.tgz#1fd01ea93b169afccafcc4f9d6fb15ad992e6ffd"
+ integrity sha512-noVd6eJE7pmBl886TahPuUdLiVkyE9ecqQhcltnZfMBCuJXeEev0/E642HRp5P2hE6uE2pBzkTAy8gaHExeY+Q==
+ dependencies:
+ "@babel/core" "^7.12.10"
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/channel-postmessage" "6.5.3"
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/preview-web" "6.5.3"
+ "@storybook/router" "6.5.3"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/store" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ "@types/node" "^14.0.10 || ^16.0.0"
+ babel-loader "^8.0.0"
+ babel-plugin-named-exports-order "^0.0.2"
+ browser-assert "^1.2.1"
+ case-sensitive-paths-webpack-plugin "^2.3.0"
+ core-js "^3.8.2"
+ css-loader "^5.0.1"
+ fork-ts-checker-webpack-plugin "^6.0.4"
+ glob "^7.1.6"
+ glob-promise "^3.4.0"
+ html-webpack-plugin "^5.0.0"
+ path-browserify "^1.0.1"
+ process "^0.11.10"
+ stable "^0.1.8"
+ style-loader "^2.0.0"
+ terser-webpack-plugin "^5.0.3"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+ webpack "^5.9.0"
+ webpack-dev-middleware "^4.1.0"
+ webpack-hot-middleware "^2.25.1"
+ webpack-virtual-modules "^0.4.1"
+
+"@storybook/channel-postmessage@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/channel-postmessage/-/channel-postmessage-6.5.3.tgz#c3762b92e87cb15e3a3c50d8a23ddede80a1e6d0"
+ integrity sha512-1vsKhFuTX53VmRm4ZKae+9z6FciSTyywZJ5cYmH2nTRWqW5GOm3UndixHzXpddVM1DWdEH4jJ/Cn15SzPRWiPg==
+ dependencies:
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ qs "^6.10.0"
+ telejson "^6.0.8"
+
+"@storybook/channel-websocket@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/channel-websocket/-/channel-websocket-6.5.3.tgz#3403d30c107763db311b084a0f6586c755ca64f0"
+ integrity sha512-Q1XCqtVMZFP1WG+OtzJ5l0Ip8umzBOkVmH3SH+DDU+o+MCSSfXKbw7UnbDUaZHzOHuFq55WaXVEnzRkeydI9rQ==
+ dependencies:
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ telejson "^6.0.8"
+
+"@storybook/channels@6.4.19":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.4.19.tgz#095bbaee494bf5b03f7cb92d34626f2f5063cb31"
+ integrity sha512-EwyoncFvTfmIlfsy8jTfayCxo2XchPkZk/9txipugWSmc057HdklMKPLOHWP0z5hLH0IbVIKXzdNISABm36jwQ==
+ dependencies:
+ core-js "^3.8.2"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/channels@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-6.5.3.tgz#d1c94f1760a27026d16705ab28a82e582cf9f2d5"
+ integrity sha512-wpxnMt5clUy+04o+I5LVMoQkYt7nc0e5PDz+pAtlNOvQaoFvlC7oQqsVYxxs1cYm6ZGqAJcsfecI5COtnQfT1w==
+ dependencies:
+ core-js "^3.8.2"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/client-api@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/client-api/-/client-api-6.5.3.tgz#ca7b5ddb9e745b1485d8946dd14d1e81173dd712"
+ integrity sha512-BxksIgSDkkt9muA41VbsSB96/u3HJAWuOJw+GCzt0yHmlBgfb3+GpQOJUDqTluWQlojg0DHJhAKgYKbejyEpIA==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/channel-postmessage" "6.5.3"
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/store" "6.5.3"
+ "@types/qs" "^6.9.5"
+ "@types/webpack-env" "^1.16.0"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+ qs "^6.10.0"
+ regenerator-runtime "^0.13.7"
+ store2 "^2.12.0"
+ synchronous-promise "^2.0.15"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/client-logger@6.4.19", "@storybook/client-logger@^6.4.0 || >=6.5.0-0":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.4.19.tgz#b2011ad2fa446cce4a9afdb41974b2a576e9fad2"
+ integrity sha512-zmg/2wyc9W3uZrvxaW4BfHcr40J0v7AGslqYXk9H+ERLVwIvrR4NhxQFaS6uITjBENyRDxwzfU3Va634WcmdDQ==
+ dependencies:
+ core-js "^3.8.2"
+ global "^4.4.0"
+
+"@storybook/client-logger@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-6.5.3.tgz#4794e87d85c03aa6e38efc4c5775b9680cc1bf23"
+ integrity sha512-gUJUkDzQdOQBfAQSJffKlZQ6ueUANjTN6u4xA/FIfJM7+I5N43UuS3dFGEjcnZISS5sj7765ct2aZinMzf1NNQ==
+ dependencies:
+ core-js "^3.8.2"
+ global "^4.4.0"
+
+"@storybook/components@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/components/-/components-6.5.3.tgz#8d643d2d89c298e5e5c2957f7ffb171ba3288ecf"
+ integrity sha512-vYTsg9ADzkPeTsmN1bm351wGqq+oyb8SrAJzLe+FXN+dujIIA9sGEQb6eUZdGe121RDgTyFMO2zKurcJNnGnxQ==
+ dependencies:
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/theming" "6.5.3"
+ "@types/react-syntax-highlighter" "11.0.5"
+ core-js "^3.8.2"
+ qs "^6.10.0"
+ react-syntax-highlighter "^15.4.5"
+ regenerator-runtime "^0.13.7"
+ util-deprecate "^1.0.2"
+
+"@storybook/core-client@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/core-client/-/core-client-6.5.3.tgz#9c689a5f6797dc7afe60183c29502e496bbd0a07"
+ integrity sha512-tsyXs+J7e210lRWminzyQU5uvbiKq9XrzsMs6feGyCE3kjZbBCj7RIgd/KxStVT/Ssim6BeTXHfnlLTxLbq5pQ==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/channel-postmessage" "6.5.3"
+ "@storybook/channel-websocket" "6.5.3"
+ "@storybook/client-api" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/preview-web" "6.5.3"
+ "@storybook/store" "6.5.3"
+ "@storybook/ui" "6.5.3"
+ airbnb-js-shims "^2.2.1"
+ ansi-to-html "^0.6.11"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ qs "^6.10.0"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+ unfetch "^4.2.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/core-common@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/core-common/-/core-common-6.5.3.tgz#5892e1ebc77f11b1058b716cb514bc2d1d5636ef"
+ integrity sha512-A0WJDm/Eo2eP8CZeAQPo8oep+Pbbm/uU6Gl/8GsvK7AVtDv/Sm/kyAWcvqMx5ovnZ7A+qYYjvHr90EZuDoltPg==
+ dependencies:
+ "@babel/core" "^7.12.10"
+ "@babel/plugin-proposal-class-properties" "^7.12.1"
+ "@babel/plugin-proposal-decorators" "^7.12.12"
+ "@babel/plugin-proposal-export-default-from" "^7.12.1"
+ "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1"
+ "@babel/plugin-proposal-object-rest-spread" "^7.12.1"
+ "@babel/plugin-proposal-optional-chaining" "^7.12.7"
+ "@babel/plugin-proposal-private-methods" "^7.12.1"
+ "@babel/plugin-proposal-private-property-in-object" "^7.12.1"
+ "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+ "@babel/plugin-transform-arrow-functions" "^7.12.1"
+ "@babel/plugin-transform-block-scoping" "^7.12.12"
+ "@babel/plugin-transform-classes" "^7.12.1"
+ "@babel/plugin-transform-destructuring" "^7.12.1"
+ "@babel/plugin-transform-for-of" "^7.12.1"
+ "@babel/plugin-transform-parameters" "^7.12.1"
+ "@babel/plugin-transform-shorthand-properties" "^7.12.1"
+ "@babel/plugin-transform-spread" "^7.12.1"
+ "@babel/preset-env" "^7.12.11"
+ "@babel/preset-react" "^7.12.10"
+ "@babel/preset-typescript" "^7.12.7"
+ "@babel/register" "^7.12.1"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/semver" "^7.3.2"
+ "@types/node" "^14.0.10 || ^16.0.0"
+ "@types/pretty-hrtime" "^1.0.0"
+ babel-loader "^8.0.0"
+ babel-plugin-macros "^3.0.1"
+ babel-plugin-polyfill-corejs3 "^0.1.0"
+ chalk "^4.1.0"
+ core-js "^3.8.2"
+ express "^4.17.1"
+ file-system-cache "^1.0.5"
+ find-up "^5.0.0"
+ fork-ts-checker-webpack-plugin "^6.0.4"
+ fs-extra "^9.0.1"
+ glob "^7.1.6"
+ handlebars "^4.7.7"
+ interpret "^2.2.0"
+ json5 "^2.1.3"
+ lazy-universal-dotenv "^3.0.1"
+ picomatch "^2.3.0"
+ pkg-dir "^5.0.0"
+ pretty-hrtime "^1.0.3"
+ resolve-from "^5.0.0"
+ slash "^3.0.0"
+ telejson "^6.0.8"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+ webpack "4"
+
+"@storybook/core-events@6.4.19":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.4.19.tgz#d2a03156783a3cb9bd9f7ba81a06a798a5c296ae"
+ integrity sha512-KICzUw6XVQUJzFSCXfvhfHAuyhn4Q5J4IZEfuZkcGJS4ODkrO6tmpdYE5Cfr+so95Nfp0ErWiLUuodBsW9/rtA==
+ dependencies:
+ core-js "^3.8.2"
+
+"@storybook/core-events@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-6.5.3.tgz#57fb45187d6d60403149d9b16953989897e2bd6e"
+ integrity sha512-DTWFjXJIx+sZndv3lsJohVEJoUL5MgtkSeeKaypkJmZm9kXkylhA0NnA07CMRE6GMqCWw6NYGSe+qOEGsHj5ig==
+ dependencies:
+ core-js "^3.8.2"
+
+"@storybook/core-server@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/core-server/-/core-server-6.5.3.tgz#6c6168ec74f07594b48b248ae53454745945b37e"
+ integrity sha512-aehEen3VeY2NvouYfbnw346KtRwCJceOH5IWGHvVVEauSVzwCH+Yfgy7c4k2j0Ey3u6fz5qCkvPp8rdY0XS7SA==
+ dependencies:
+ "@discoveryjs/json-ext" "^0.5.3"
+ "@storybook/builder-webpack4" "6.5.3"
+ "@storybook/core-client" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/csf-tools" "6.5.3"
+ "@storybook/manager-webpack4" "6.5.3"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/store" "6.5.3"
+ "@storybook/telemetry" "6.5.3"
+ "@types/node" "^14.0.10 || ^16.0.0"
+ "@types/node-fetch" "^2.5.7"
+ "@types/pretty-hrtime" "^1.0.0"
+ "@types/webpack" "^4.41.26"
+ better-opn "^2.1.1"
+ boxen "^5.1.2"
+ chalk "^4.1.0"
+ cli-table3 "^0.6.1"
+ commander "^6.2.1"
+ compression "^1.7.4"
+ core-js "^3.8.2"
+ cpy "^8.1.2"
+ detect-port "^1.3.0"
+ express "^4.17.1"
+ fs-extra "^9.0.1"
+ global "^4.4.0"
+ globby "^11.0.2"
+ ip "^1.1.5"
+ lodash "^4.17.21"
+ node-fetch "^2.6.7"
+ open "^8.4.0"
+ pretty-hrtime "^1.0.3"
+ prompts "^2.4.0"
+ regenerator-runtime "^0.13.7"
+ serve-favicon "^2.5.0"
+ slash "^3.0.0"
+ telejson "^6.0.8"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+ watchpack "^2.2.0"
+ webpack "4"
+ ws "^8.2.3"
+ x-default-browser "^0.4.0"
+
+"@storybook/core@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/core/-/core-6.5.3.tgz#8291e7bccd5d726ac7dc39c1e27fc04178f777f6"
+ integrity sha512-XQDcAryLNyXe5eiNqB++6xvGqnYlJ8ZAFOPWFlFUhjrktojtwVEeHfj5M3e23D9XMN4KkBODoH3OWmREcUMwXg==
+ dependencies:
+ "@storybook/core-client" "6.5.3"
+ "@storybook/core-server" "6.5.3"
+
+"@storybook/csf-tools@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/csf-tools/-/csf-tools-6.5.3.tgz#4f3b53bbe38f4a62e1ed07948a632512fb59f41f"
+ integrity sha512-WotBTvKauVV+i2DZqem4m12D+Ogexg6oFiXt0dlqh0TUGEAGzvocOAPIKk6uciEF2eXu6yn8JE4s+faXLWrXSw==
+ dependencies:
+ "@babel/core" "^7.12.10"
+ "@babel/generator" "^7.12.11"
+ "@babel/parser" "^7.12.11"
+ "@babel/plugin-transform-react-jsx" "^7.12.12"
+ "@babel/preset-env" "^7.12.11"
+ "@babel/traverse" "^7.12.11"
+ "@babel/types" "^7.12.11"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/mdx1-csf" canary
+ core-js "^3.8.2"
+ fs-extra "^9.0.1"
+ global "^4.4.0"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+
+"@storybook/csf@0.0.2--canary.4566f4d.1":
+ version "0.0.2--canary.4566f4d.1"
+ resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.2--canary.4566f4d.1.tgz#dac52a21c40ef198554e71fe4d20d61e17f65327"
+ integrity sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==
+ dependencies:
+ lodash "^4.17.15"
+
+"@storybook/csf@0.0.2--canary.87bc651.0":
+ version "0.0.2--canary.87bc651.0"
+ resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.2--canary.87bc651.0.tgz#c7b99b3a344117ef67b10137b6477a3d2750cf44"
+ integrity sha512-ajk1Uxa+rBpFQHKrCcTmJyQBXZ5slfwHVEaKlkuFaW77it8RgbPJp/ccna3sgoi8oZ7FkkOyvv1Ve4SmwFqRqw==
+ dependencies:
+ lodash "^4.17.15"
+
+"@storybook/csf@^0.0.1":
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.1.tgz#95901507dc02f0bc6f9ac8ee1983e2fc5bb98ce6"
+ integrity sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==
+ dependencies:
+ lodash "^4.17.15"
+
+"@storybook/docs-tools@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/docs-tools/-/docs-tools-6.5.3.tgz#2460df5d20e4c6670e985268b50bf9c412eb47b7"
+ integrity sha512-scUztkQ9ZRRoo4lHiYRaCkmk351H2CwMnlOrCwv/EpmLZnHdffSAtMZS/O07KUOC8fvxCw36z5SfHlbCIcolSQ==
+ dependencies:
+ "@babel/core" "^7.12.10"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/store" "6.5.3"
+ core-js "^3.8.2"
+ doctrine "^3.0.0"
+ lodash "^4.17.21"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/instrumenter@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-6.5.3.tgz#9c72d52ca6a1fd4db2dca8792cc42a797c7e476f"
+ integrity sha512-Dh7EjqbhKowFlmfBLdgq0OTBgm3HQbhzs82q9oM2sCcXJJyjlYSdne/JDuWY2NDGV30aiwqvCGkvr/w1OUh/xg==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ core-js "^3.8.2"
+ global "^4.4.0"
+
+"@storybook/instrumenter@^6.4.0 || >=6.5.0-0":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-6.4.19.tgz#1586624d0315713c20f3d7b4e2c7fc595730b934"
+ integrity sha512-KwOJUW7tItZw1CLffUbaSfEzH1p1HdGmJs1Q42uWGJSbXbVHZ7i7bJWYb3Lf90+yHTlqQjTr1PKAymwDwHseTA==
+ dependencies:
+ "@storybook/addons" "6.4.19"
+ "@storybook/client-logger" "6.4.19"
+ "@storybook/core-events" "6.4.19"
+ global "^4.4.0"
+
+"@storybook/manager-webpack4@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/manager-webpack4/-/manager-webpack4-6.5.3.tgz#004380031a521f50d5d37740e7d2add3342dbd90"
+ integrity sha512-kI+6fbCHv9uH1GEYK/SYLtNiAJYVRgm8oh/7Zi/38cqUjnAg8lzUztABul4Iemu3w5aJplkqE08FFu69TTbHPg==
+ dependencies:
+ "@babel/core" "^7.12.10"
+ "@babel/plugin-transform-template-literals" "^7.12.1"
+ "@babel/preset-react" "^7.12.10"
+ "@storybook/addons" "6.5.3"
+ "@storybook/core-client" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ "@storybook/ui" "6.5.3"
+ "@types/node" "^14.0.10 || ^16.0.0"
+ "@types/webpack" "^4.41.26"
+ babel-loader "^8.0.0"
+ case-sensitive-paths-webpack-plugin "^2.3.0"
+ chalk "^4.1.0"
+ core-js "^3.8.2"
+ css-loader "^3.6.0"
+ express "^4.17.1"
+ file-loader "^6.2.0"
+ find-up "^5.0.0"
+ fs-extra "^9.0.1"
+ html-webpack-plugin "^4.0.0"
+ node-fetch "^2.6.7"
+ pnp-webpack-plugin "1.6.4"
+ read-pkg-up "^7.0.1"
+ regenerator-runtime "^0.13.7"
+ resolve-from "^5.0.0"
+ style-loader "^1.3.0"
+ telejson "^6.0.8"
+ terser-webpack-plugin "^4.2.3"
+ ts-dedent "^2.0.0"
+ url-loader "^4.1.1"
+ util-deprecate "^1.0.2"
+ webpack "4"
+ webpack-dev-middleware "^3.7.3"
+ webpack-virtual-modules "^0.2.2"
+
+"@storybook/manager-webpack5@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/manager-webpack5/-/manager-webpack5-6.5.3.tgz#63cd2fe547653c5489f301db954016bed4ab9d29"
+ integrity sha512-AJpx2cpiBXt6lVHxe2ZaBWTXqq8/GNXfFKXuBRTvLM0ZqiY3HcohUmgpWs56OC2JUwunbZiS0nvxA3dzQmuBZg==
+ dependencies:
+ "@babel/core" "^7.12.10"
+ "@babel/plugin-transform-template-literals" "^7.12.1"
+ "@babel/preset-react" "^7.12.10"
+ "@storybook/addons" "6.5.3"
+ "@storybook/core-client" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/theming" "6.5.3"
+ "@storybook/ui" "6.5.3"
+ "@types/node" "^14.0.10 || ^16.0.0"
+ babel-loader "^8.0.0"
+ case-sensitive-paths-webpack-plugin "^2.3.0"
+ chalk "^4.1.0"
+ core-js "^3.8.2"
+ css-loader "^5.0.1"
+ express "^4.17.1"
+ find-up "^5.0.0"
+ fs-extra "^9.0.1"
+ html-webpack-plugin "^5.0.0"
+ node-fetch "^2.6.7"
+ process "^0.11.10"
+ read-pkg-up "^7.0.1"
+ regenerator-runtime "^0.13.7"
+ resolve-from "^5.0.0"
+ style-loader "^2.0.0"
+ telejson "^6.0.8"
+ terser-webpack-plugin "^5.0.3"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+ webpack "^5.9.0"
+ webpack-dev-middleware "^4.1.0"
+ webpack-virtual-modules "^0.4.1"
+
+"@storybook/mdx1-csf@canary":
+ version "0.0.1-canary.1.867dcd5.0"
+ resolved "https://registry.yarnpkg.com/@storybook/mdx1-csf/-/mdx1-csf-0.0.1-canary.1.867dcd5.0.tgz#e8739a7451a557292977d83bfb7475986a8013b6"
+ integrity sha512-VnlE825M9SpjyJCPLCXbo+RbvqllsqXqRDCouzHKSpCE3Q79KR7MMURBsJo/vrTG1zeNG68Z4TZrLAu6IoyYaA==
+ dependencies:
+ "@babel/generator" "^7.12.11"
+ "@babel/parser" "^7.12.11"
+ "@babel/preset-env" "^7.12.11"
+ "@babel/types" "^7.12.11"
+ "@mdx-js/mdx" "^1.6.22"
+ "@types/lodash" "^4.14.167"
+ js-string-escape "^1.0.1"
+ loader-utils "^2.0.0"
+ lodash "^4.17.21"
+ prettier ">=2.2.1 <=2.3.0"
+ ts-dedent "^2.0.0"
+
+"@storybook/node-logger@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/node-logger/-/node-logger-6.5.3.tgz#6917bbbdaf2bb3b191bafadfe4f19c3ea3cbfa61"
+ integrity sha512-iG4uQJCtuT54p3zg0zJ7+ALPUrt7PTAXmXqN7ak/9YcWbnwtMlHgg8oTlCebwr+E3QPCMauJM2eLzC6F7bI76w==
+ dependencies:
+ "@types/npmlog" "^4.1.2"
+ chalk "^4.1.0"
+ core-js "^3.8.2"
+ npmlog "^5.0.1"
+ pretty-hrtime "^1.0.3"
+
+"@storybook/postinstall@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/postinstall/-/postinstall-6.5.3.tgz#ba815b7590d0fe8ee8626e62ddd3f729264aa464"
+ integrity sha512-EIHnbrGwt/CPyLXwNFyfAGY4LhrBx713ghCtacQu2xAukDXg9UWJHarYXjwIivk8DGrims28qXGIjIUKqKeuyA==
+ dependencies:
+ core-js "^3.8.2"
+
+"@storybook/preview-web@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/preview-web/-/preview-web-6.5.3.tgz#f54fb17476c6a57d715ad2e46674b6b668e6729f"
+ integrity sha512-NI+sKFloj0vP1xAMaF1BhOAokB2u0qZ5rxx8lnU8eBmAukRURGoebuHWohBoQqniuupaNQK5hkUlJ3mkAPZi8Q==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/channel-postmessage" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/store" "6.5.3"
+ ansi-to-html "^0.6.11"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ qs "^6.10.0"
+ regenerator-runtime "^0.13.7"
+ synchronous-promise "^2.0.15"
+ ts-dedent "^2.0.0"
+ unfetch "^4.2.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/react-docgen-typescript-plugin@1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0":
+ version "1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0"
+ resolved "https://registry.yarnpkg.com/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0.tgz#3103532ff494fb7dc3cf835f10740ecf6a26c0f9"
+ integrity sha512-eVg3BxlOm2P+chijHBTByr90IZVUtgRW56qEOLX7xlww2NBuKrcavBlcmn+HH7GIUktquWkMPtvy6e0W0NgA5w==
+ dependencies:
+ debug "^4.1.1"
+ endent "^2.0.1"
+ find-cache-dir "^3.3.1"
+ flat-cache "^3.0.4"
+ micromatch "^4.0.2"
+ react-docgen-typescript "^2.1.1"
+ tslib "^2.0.0"
+
+"@storybook/react@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.5.3.tgz#bab65edd22719d3367e136db20a79bd09aab41a0"
+ integrity sha512-RP9ak2EIrq9sJ80JjaVV/Xab9O663PA/DRqfbILewRLi9uUaG6L/Qby7LktwzqwaybeKbP6dTG0w937cRkuj4w==
+ dependencies:
+ "@babel/preset-flow" "^7.12.1"
+ "@babel/preset-react" "^7.12.10"
+ "@pmmmwh/react-refresh-webpack-plugin" "^0.5.3"
+ "@storybook/addons" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ "@storybook/docs-tools" "6.5.3"
+ "@storybook/node-logger" "6.5.3"
+ "@storybook/react-docgen-typescript-plugin" "1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/store" "6.5.3"
+ "@types/estree" "^0.0.51"
+ "@types/node" "^14.14.20 || ^16.0.0"
+ "@types/webpack-env" "^1.16.0"
+ acorn "^7.4.1"
+ acorn-jsx "^5.3.1"
+ acorn-walk "^7.2.0"
+ babel-plugin-add-react-displayname "^0.0.5"
+ babel-plugin-react-docgen "^4.2.1"
+ core-js "^3.8.2"
+ escodegen "^2.0.0"
+ fs-extra "^9.0.1"
+ global "^4.4.0"
+ html-tags "^3.1.0"
+ lodash "^4.17.21"
+ prop-types "^15.7.2"
+ react-element-to-jsx-string "^14.3.4"
+ react-refresh "^0.11.0"
+ read-pkg-up "^7.0.1"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+ webpack ">=4.43.0 <6.0.0"
+
+"@storybook/router@6.4.19":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.4.19.tgz#e653224dd9a521836bbd2610f604f609a2c77af2"
+ integrity sha512-KWWwIzuyeEIWVezkCihwY2A76Il9tUNg0I410g9qT7NrEsKyqXGRYOijWub7c1GGyNjLqz0jtrrehtixMcJkuA==
+ dependencies:
+ "@storybook/client-logger" "6.4.19"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ history "5.0.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+ qs "^6.10.0"
+ react-router "^6.0.0"
+ react-router-dom "^6.0.0"
+ ts-dedent "^2.0.0"
+
+"@storybook/router@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/router/-/router-6.5.3.tgz#55d8a0a5c3acdef9695482e38cd4d70f6926c261"
+ integrity sha512-UcErvdeuCTMYvmztDogrTK1DKQ8ZFkUR/46bEuVo4tg9OzlX3fr+JqD4RZHT4YOUYmDcTm6cLlUJhDalUpoU6Q==
+ dependencies:
+ "@storybook/client-logger" "6.5.3"
+ core-js "^3.8.2"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/semver@^7.3.2":
+ version "7.3.2"
+ resolved "https://registry.yarnpkg.com/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0"
+ integrity sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==
+ dependencies:
+ core-js "^3.6.5"
+ find-up "^4.1.0"
+
+"@storybook/source-loader@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/source-loader/-/source-loader-6.5.3.tgz#5991dd33805ebb54c5c1e0ad779a2cf80ec6b3b9"
+ integrity sha512-JrwCm3El6XZC7eVYCF83e7x7/fA4ue+g2s0oAtdXD11KOrrJ7e0bgtvVdtRWKG/4n4Ww3+sGFnuIlXfPbJ3hvw==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ core-js "^3.8.2"
+ estraverse "^5.2.0"
+ global "^4.4.0"
+ loader-utils "^2.0.0"
+ lodash "^4.17.21"
+ prettier ">=2.2.1 <=2.3.0"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/store@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/store/-/store-6.5.3.tgz#8bde9d3ad071c3612665b6190ed093784a98d22d"
+ integrity sha512-vI5w3OlDsCQE32C5AekRfHI6qX7s7iKRAUJKQE4Azqch37EAnMNLWE3E13KAzdLX1oU+JNRGHjJTogsQUR2UeQ==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/csf" "0.0.2--canary.4566f4d.1"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+ regenerator-runtime "^0.13.7"
+ slash "^3.0.0"
+ stable "^0.1.8"
+ synchronous-promise "^2.0.15"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
+"@storybook/telemetry@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/telemetry/-/telemetry-6.5.3.tgz#2440606fa82406d279b0d60261613c5038d3bfbf"
+ integrity sha512-eXhQ+kRWdg+ZX3sglRaCoOzpzgQ/p9wKS4/vEJd5Fq+JlKIuZkJ7xiJYaxlhR55PLpJGbj+5HCTlnepAtf7Nnw==
+ dependencies:
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/core-common" "6.5.3"
+ chalk "^4.1.0"
+ core-js "^3.8.2"
+ detect-package-manager "^2.0.1"
+ fetch-retry "^5.0.2"
+ fs-extra "^9.0.1"
+ global "^4.4.0"
+ isomorphic-unfetch "^3.1.0"
+ nanoid "^3.3.1"
+ read-pkg-up "^7.0.1"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/testing-library@^0.0.11":
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/@storybook/testing-library/-/testing-library-0.0.11.tgz#c07a3b5a76049ea9f9e66c557b506c2b061a93d6"
+ integrity sha512-8KbKx3s1e+uF3oWlPdyXRpZa6xtCsCHtXh1nCTisMA6P5YcSDaCg59NXIOVIQCAwKvjRomlqMJH8JL1WyOzeVg==
+ dependencies:
+ "@storybook/client-logger" "^6.4.0 || >=6.5.0-0"
+ "@storybook/instrumenter" "^6.4.0 || >=6.5.0-0"
+ "@testing-library/dom" "^8.3.0"
+ "@testing-library/user-event" "^13.2.1"
+ ts-dedent "^2.2.0"
+
+"@storybook/theming@6.4.19":
+ version "6.4.19"
+ resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.4.19.tgz#0a6834d91e0b0eadbb10282e7fb2947e2bbf9e9e"
+ integrity sha512-V4pWmTvAxmbHR6B3jA4hPkaxZPyExHvCToy7b76DpUTpuHihijNDMAn85KhOQYIeL9q14zP/aiz899tOHsOidg==
+ dependencies:
+ "@emotion/core" "^10.1.1"
+ "@emotion/is-prop-valid" "^0.8.6"
+ "@emotion/styled" "^10.0.27"
+ "@storybook/client-logger" "6.4.19"
+ core-js "^3.8.2"
+ deep-object-diff "^1.1.0"
+ emotion-theming "^10.0.27"
+ global "^4.4.0"
+ memoizerific "^1.11.3"
+ polished "^4.0.5"
+ resolve-from "^5.0.0"
+ ts-dedent "^2.0.0"
+
+"@storybook/theming@6.5.3", "@storybook/theming@^6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-6.5.3.tgz#a8df53df2812d49453a3410346123231e028c103"
+ integrity sha512-2tM46jahAhKRUzCcoaqPoqs+4imXqbze0dCPZ0cdVnfs14jhMB1lAfGE+diodCCaUcXUu8r2c5dTPKqqM1lHqQ==
+ dependencies:
+ "@storybook/client-logger" "6.5.3"
+ core-js "^3.8.2"
+ regenerator-runtime "^0.13.7"
+
+"@storybook/ui@6.5.3":
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/@storybook/ui/-/ui-6.5.3.tgz#afa7a327c71350e9f92628b86c7f14d58427ec3c"
+ integrity sha512-TLJBfXHFM0ilMuUjer4AjhnNGvQ7lI4GYIKzuCjCrw/ukfUb1AABXd9fdHq7tEzVm8z7T3pyrRVsb1VVdbxL0A==
+ dependencies:
+ "@storybook/addons" "6.5.3"
+ "@storybook/api" "6.5.3"
+ "@storybook/channels" "6.5.3"
+ "@storybook/client-logger" "6.5.3"
+ "@storybook/components" "6.5.3"
+ "@storybook/core-events" "6.5.3"
+ "@storybook/router" "6.5.3"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/theming" "6.5.3"
+ core-js "^3.8.2"
+ regenerator-runtime "^0.13.7"
+ resolve-from "^5.0.0"
+
"@svgr/babel-plugin-add-jsx-attribute@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.0.0.tgz#bd6d1ff32a31b82b601e73672a789cc41e84fe18"
@@ -1790,6 +3370,20 @@
"@svgr/plugin-jsx" "^6.2.1"
"@svgr/plugin-svgo" "^6.2.0"
+"@testing-library/dom@^8.3.0":
+ version "8.12.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.12.0.tgz#fef5e545533fb084175dda6509ee71d7d2f72e23"
+ integrity sha512-rBrJk5WjI02X1edtiUcZhgyhgBhiut96r5Jp8J5qktKdcvLcZpKDW8i2hkGMMItxrghjXuQ5AM6aE0imnFawaw==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/runtime" "^7.12.5"
+ "@types/aria-query" "^4.2.0"
+ aria-query "^5.0.0"
+ chalk "^4.1.0"
+ dom-accessibility-api "^0.5.9"
+ lz-string "^1.4.4"
+ pretty-format "^27.0.2"
+
"@testing-library/dom@^8.5.0":
version "8.13.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5"
@@ -1828,6 +3422,13 @@
"@testing-library/dom" "^8.5.0"
"@types/react-dom" "^18.0.0"
+"@testing-library/user-event@^13.2.1":
+ version "13.5.0"
+ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295"
+ integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+
"@testing-library/user-event@^14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.1.0.tgz#db479c06271b72a4d41cf595ec2ad7ff078c1d72"
@@ -1920,7 +3521,15 @@
dependencies:
"@types/ms" "*"
-"@types/eslint@7 || 8":
+"@types/eslint-scope@^3.7.3":
+ version "3.7.3"
+ resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
+ integrity sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==
+ dependencies:
+ "@types/eslint" "*"
+ "@types/estree" "*"
+
+"@types/eslint@*", "@types/eslint@7 || 8":
version "8.4.1"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.1.tgz#c48251553e8759db9e656de3efc846954ac32304"
integrity sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==
@@ -1935,7 +3544,7 @@
dependencies:
"@types/estree" "*"
-"@types/estree@*":
+"@types/estree@*", "@types/estree@^0.0.51":
version "0.0.51"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40"
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
@@ -1957,6 +3566,14 @@
dependencies:
"@types/node" "*"
+"@types/glob@*", "@types/glob@^7.1.1":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
+ integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+ dependencies:
+ "@types/minimatch" "*"
+ "@types/node" "*"
+
"@types/graceful-fs@^4.1.2":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.5.tgz#21ffba0d98da4350db64891f92a9e5db3cdb4e15"
@@ -1979,6 +3596,21 @@
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
+"@types/html-minifier-terser@^5.0.0":
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57"
+ integrity sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==
+
+"@types/html-minifier-terser@^6.0.0":
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35"
+ integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==
+
+"@types/is-function@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.1.tgz#2d024eace950c836d9e3335a66b97960ae41d022"
+ integrity sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q==
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
@@ -2006,7 +3638,7 @@
jest-matcher-utils "^27.0.0"
pretty-format "^27.0.0"
-"@types/json-schema@*", "@types/json-schema@^7.0.9":
+"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
@@ -2021,6 +3653,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
+"@types/lodash@^4.14.167":
+ version "4.14.182"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2"
+ integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
+
"@types/mdast@^3.0.0":
version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@@ -2038,6 +3675,11 @@
resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.1.tgz#e4c05d355d092d7b58db1abfe460e53f41102ac8"
integrity sha512-JPEv4iAl0I+o7g8yVWDwk30es8mfVrjkvh5UeVR2sYPpZCK44vrAPsbJpIS+rJAUxLgaSAMKTEH5Vn5qd9XsrQ==
+"@types/minimatch@*":
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+ integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
+
"@types/minimist@^1.2.0":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
@@ -2048,6 +3690,14 @@
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
+"@types/node-fetch@^2.5.7":
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
+ integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
+ dependencies:
+ "@types/node" "*"
+ form-data "^3.0.0"
+
"@types/node@*", "@types/node@14 || 16 || 17", "@types/node@>=12", "@types/node@^17.0.24":
version "17.0.24"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.24.tgz#20ba1bf69c1b4ab405c7a01e950c4f446b05029f"
@@ -2058,21 +3708,41 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.13.tgz#6ad4d9db59e6b3faf98dcfe4ca9d2aec84443277"
integrity sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA==
+"@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0":
+ version "16.11.36"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.36.tgz#9ab9f8276987132ed2b225cace2218ba794fc751"
+ integrity sha512-FR5QJe+TaoZ2GsMHkjuwoNabr+UrJNRr2HNOo+r/7vhcuntM6Ee/pRPOnRhhL2XE9OOvX9VLEq+BcXl3VjNoWA==
+
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
+"@types/npmlog@^4.1.2":
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.4.tgz#30eb872153c7ead3e8688c476054ddca004115f6"
+ integrity sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==
+
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+"@types/parse5@^5.0.0":
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
+ integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==
+
"@types/prettier@^2.1.5":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.6.0.tgz#efcbd41937f9ae7434c714ab698604822d890759"
integrity sha512-G/AdOadiZhnJp0jXCaBQU449W2h716OW/EoXeYkCytxKL06X1WCXB4DZpp8TpZ8eyIJVS1cw4lrlkkSYU21cDw==
+"@types/pretty-hrtime@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@types/pretty-hrtime/-/pretty-hrtime-1.0.1.tgz#72a26101dc567b0d68fd956cf42314556e42d601"
+ integrity sha512-VjID5MJb1eGKthz2qUerWT8+R4b9N+CHvGCzg9fn4kWZgaF9AhdYikQio3R7wV8YY1NsQKPaCwKz1Yff+aHNUQ==
+
"@types/prismjs@^1.26.0":
version "1.26.0"
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654"
@@ -2083,6 +3753,11 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
+"@types/qs@^6.9.5":
+ version "6.9.7"
+ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
+ integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
+
"@types/react-dom@^18.0.0":
version "18.0.1"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.1.tgz#cb3cc10ea91141b12c71001fede1017acfbce4db"
@@ -2090,7 +3765,14 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@>=16", "@types/react@^18.0.5":
+"@types/react-syntax-highlighter@11.0.5":
+ version "11.0.5"
+ resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.5.tgz#0d546261b4021e1f9d85b50401c0a42acb106087"
+ integrity sha512-VIOi9i2Oj5XsmWWoB72p3KlZoEbdRAcechJa8Ztebw7bDl2YmR+odxIqhtJGp1q2EozHs02US+gzxJ9nuf56qg==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react@*", "@types/react@16 || 17 || 18", "@types/react@>=16", "@types/react@^18.0.5":
version "18.0.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.5.tgz#1a4d4b705ae6af5aed369dec22800b20f89f5301"
integrity sha512-UPxNGInDCIKlfqBrm8LDXYWNfLHwIdisWcsH5GpMyGjhEDLFgTtlRBaoWuCua9HcyuE0rMkmAeZ3FXV1pYLIYQ==
@@ -2099,25 +3781,26 @@
"@types/scheduler" "*"
csstype "^3.0.2"
-"@types/react@16 || 17":
- version "17.0.44"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.44.tgz#c3714bd34dd551ab20b8015d9d0dbec812a51ec7"
- integrity sha512-Ye0nlw09GeMp2Suh8qoOv0odfgCoowfM/9MG6WeRD60Gq9wS90bdkdRtYbRkNhXOpG4H+YXGvj4wOWhAC0LJ1g==
- dependencies:
- "@types/prop-types" "*"
- "@types/scheduler" "*"
- csstype "^3.0.2"
-
"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
+"@types/source-list-map@*":
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
+ integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
+
"@types/stack-utils@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
+"@types/tapable@^1", "@types/tapable@^1.0.5":
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
+ integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==
+
"@types/testing-library__jest-dom@^5.9.1":
version "5.14.3"
resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17"
@@ -2125,7 +3808,14 @@
dependencies:
"@types/jest" "*"
-"@types/unist@*", "@types/unist@^2.0.0":
+"@types/uglify-js@*":
+ version "3.13.1"
+ resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea"
+ integrity sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==
+ dependencies:
+ source-map "^0.6.1"
+
+"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2", "@types/unist@^2.0.3":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
@@ -2137,11 +3827,44 @@
dependencies:
"@types/ackee-tracker" "*"
+"@types/webpack-env@^1.16.0":
+ version "1.16.3"
+ resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"
+ integrity sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw==
+
+"@types/webpack-sources@*":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b"
+ integrity sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==
+ dependencies:
+ "@types/node" "*"
+ "@types/source-list-map" "*"
+ source-map "^0.7.3"
+
+"@types/webpack@^4.41.26", "@types/webpack@^4.41.8":
+ version "4.41.32"
+ resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.32.tgz#a7bab03b72904070162b2f169415492209e94212"
+ integrity sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==
+ dependencies:
+ "@types/node" "*"
+ "@types/tapable" "^1"
+ "@types/uglify-js" "*"
+ "@types/webpack-sources" "*"
+ anymatch "^3.0.0"
+ source-map "^0.6.0"
+
"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
+"@types/yargs@^15.0.0":
+ version "15.0.14"
+ resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06"
+ integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==
+ dependencies:
+ "@types/yargs-parser" "*"
+
"@types/yargs@^16.0.0":
version "16.0.4"
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
@@ -2149,6 +3872,13 @@
dependencies:
"@types/yargs-parser" "*"
+"@typescript-eslint/experimental-utils@^5.3.0":
+ version "5.17.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.17.0.tgz#303ba1d766d715c3225a31845b54941889e52f6c"
+ integrity sha512-U4sM5z0/ymSYqQT6I7lz8l0ZZ9zrya5VIwrwAP5WOJVabVtVsIpTMxPQe+D3qLyePT+VlETUTO2nA1+PufPx9Q==
+ dependencies:
+ "@typescript-eslint/utils" "5.17.0"
+
"@typescript-eslint/parser@5.10.1":
version "5.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.10.1.tgz#4ce9633cc33fc70bc13786cb793c1a76fe5ad6bd"
@@ -2167,6 +3897,14 @@
"@typescript-eslint/types" "5.10.1"
"@typescript-eslint/visitor-keys" "5.10.1"
+"@typescript-eslint/scope-manager@5.17.0":
+ version "5.17.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.17.0.tgz#4cea7d0e0bc0e79eb60cad431c89120987c3f952"
+ integrity sha512-062iCYQF/doQ9T2WWfJohQKKN1zmmXVfAcS3xaiialiw8ZUGy05Em6QVNYJGO34/sU1a7a+90U3dUNfqUDHr3w==
+ dependencies:
+ "@typescript-eslint/types" "5.17.0"
+ "@typescript-eslint/visitor-keys" "5.17.0"
+
"@typescript-eslint/scope-manager@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.19.0.tgz#97e59b0bcbcb54dbcdfba96fc103b9020bbe9cb4"
@@ -2180,6 +3918,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.10.1.tgz#dca9bd4cb8c067fc85304a31f38ec4766ba2d1ea"
integrity sha512-ZvxQ2QMy49bIIBpTqFiOenucqUyjTQ0WNLhBM6X1fh1NNlYAC6Kxsx8bRTY3jdYsYg44a0Z/uEgQkohbR0H87Q==
+"@typescript-eslint/types@5.17.0":
+ version "5.17.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.17.0.tgz#861ec9e669ffa2aa9b873dd4d28d9b1ce26d216f"
+ integrity sha512-AgQ4rWzmCxOZLioFEjlzOI3Ch8giDWx8aUDxyNw9iOeCvD3GEYAB7dxWGQy4T/rPVe8iPmu73jPHuaSqcjKvxw==
+
"@typescript-eslint/types@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.19.0.tgz#12d3d600d754259da771806ee8b2c842d3be8d12"
@@ -2198,6 +3941,19 @@
semver "^7.3.5"
tsutils "^3.21.0"
+"@typescript-eslint/typescript-estree@5.17.0":
+ version "5.17.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.17.0.tgz#a7cba7dfc8f9cc2ac78c18584e684507df4f2488"
+ integrity sha512-X1gtjEcmM7Je+qJRhq7ZAAaNXYhTgqMkR10euC4Si6PIjb+kwEQHSxGazXUQXFyqfEXdkGf6JijUu5R0uceQzg==
+ dependencies:
+ "@typescript-eslint/types" "5.17.0"
+ "@typescript-eslint/visitor-keys" "5.17.0"
+ debug "^4.3.2"
+ globby "^11.0.4"
+ is-glob "^4.0.3"
+ semver "^7.3.5"
+ tsutils "^3.21.0"
+
"@typescript-eslint/typescript-estree@5.19.0", "@typescript-eslint/typescript-estree@^5.9.1":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.19.0.tgz#fc987b8f62883f9ea6a5b488bdbcd20d33c0025f"
@@ -2211,6 +3967,18 @@
semver "^7.3.5"
tsutils "^3.21.0"
+"@typescript-eslint/utils@5.17.0":
+ version "5.17.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.17.0.tgz#549a9e1d491c6ccd3624bc3c1b098f5cfb45f306"
+ integrity sha512-DVvndq1QoxQH+hFv+MUQHrrWZ7gQ5KcJzyjhzcqB1Y2Xes1UQQkTRPUfRpqhS8mhTWsSb2+iyvDW1Lef5DD7vA==
+ dependencies:
+ "@types/json-schema" "^7.0.9"
+ "@typescript-eslint/scope-manager" "5.17.0"
+ "@typescript-eslint/types" "5.17.0"
+ "@typescript-eslint/typescript-estree" "5.17.0"
+ eslint-scope "^5.1.1"
+ eslint-utils "^3.0.0"
+
"@typescript-eslint/utils@^5.13.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.19.0.tgz#fe87f1e3003d9973ec361ed10d36b4342f1ded1e"
@@ -2231,6 +3999,14 @@
"@typescript-eslint/types" "5.10.1"
eslint-visitor-keys "^3.0.0"
+"@typescript-eslint/visitor-keys@5.17.0":
+ version "5.17.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.17.0.tgz#52daae45c61b0211b4c81b53a71841911e479128"
+ integrity sha512-6K/zlc4OfCagUu7Am/BD5k8PSWQOgh34Nrv9Rxe2tBzlJ7uOeJ/h7ugCGDCeEZHT6k2CJBhbk9IsbkPI0uvUkA==
+ dependencies:
+ "@typescript-eslint/types" "5.17.0"
+ eslint-visitor-keys "^3.0.0"
+
"@typescript-eslint/visitor-keys@5.19.0":
version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.19.0.tgz#c84ebc7f6c744707a361ca5ec7f7f64cd85b8af6"
@@ -2329,6 +4105,282 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.33.tgz#69a8c99ceb37c1b031d5cc4aec2ff1dc77e1161e"
integrity sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg==
+"@webassemblyjs/ast@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
+ integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
+ dependencies:
+ "@webassemblyjs/helper-numbers" "1.11.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+
+"@webassemblyjs/ast@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
+ integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==
+ dependencies:
+ "@webassemblyjs/helper-module-context" "1.9.0"
+ "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+ "@webassemblyjs/wast-parser" "1.9.0"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
+ integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
+
+"@webassemblyjs/floating-point-hex-parser@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4"
+ integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==
+
+"@webassemblyjs/helper-api-error@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
+ integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
+
+"@webassemblyjs/helper-api-error@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2"
+ integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==
+
+"@webassemblyjs/helper-buffer@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
+ integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
+
+"@webassemblyjs/helper-buffer@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00"
+ integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==
+
+"@webassemblyjs/helper-code-frame@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27"
+ integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==
+ dependencies:
+ "@webassemblyjs/wast-printer" "1.9.0"
+
+"@webassemblyjs/helper-fsm@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8"
+ integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==
+
+"@webassemblyjs/helper-module-context@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07"
+ integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+
+"@webassemblyjs/helper-numbers@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
+ integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
+ dependencies:
+ "@webassemblyjs/floating-point-hex-parser" "1.11.1"
+ "@webassemblyjs/helper-api-error" "1.11.1"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
+ integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
+
+"@webassemblyjs/helper-wasm-bytecode@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790"
+ integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==
+
+"@webassemblyjs/helper-wasm-section@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
+ integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/helper-buffer" "1.11.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+ "@webassemblyjs/wasm-gen" "1.11.1"
+
+"@webassemblyjs/helper-wasm-section@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346"
+ integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/helper-buffer" "1.9.0"
+ "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+ "@webassemblyjs/wasm-gen" "1.9.0"
+
+"@webassemblyjs/ieee754@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
+ integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/ieee754@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4"
+ integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
+ integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/leb128@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95"
+ integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
+ integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
+
+"@webassemblyjs/utf8@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab"
+ integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==
+
+"@webassemblyjs/wasm-edit@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
+ integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/helper-buffer" "1.11.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+ "@webassemblyjs/helper-wasm-section" "1.11.1"
+ "@webassemblyjs/wasm-gen" "1.11.1"
+ "@webassemblyjs/wasm-opt" "1.11.1"
+ "@webassemblyjs/wasm-parser" "1.11.1"
+ "@webassemblyjs/wast-printer" "1.11.1"
+
+"@webassemblyjs/wasm-edit@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf"
+ integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/helper-buffer" "1.9.0"
+ "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+ "@webassemblyjs/helper-wasm-section" "1.9.0"
+ "@webassemblyjs/wasm-gen" "1.9.0"
+ "@webassemblyjs/wasm-opt" "1.9.0"
+ "@webassemblyjs/wasm-parser" "1.9.0"
+ "@webassemblyjs/wast-printer" "1.9.0"
+
+"@webassemblyjs/wasm-gen@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
+ integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+ "@webassemblyjs/ieee754" "1.11.1"
+ "@webassemblyjs/leb128" "1.11.1"
+ "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wasm-gen@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c"
+ integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+ "@webassemblyjs/ieee754" "1.9.0"
+ "@webassemblyjs/leb128" "1.9.0"
+ "@webassemblyjs/utf8" "1.9.0"
+
+"@webassemblyjs/wasm-opt@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
+ integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/helper-buffer" "1.11.1"
+ "@webassemblyjs/wasm-gen" "1.11.1"
+ "@webassemblyjs/wasm-parser" "1.11.1"
+
+"@webassemblyjs/wasm-opt@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61"
+ integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/helper-buffer" "1.9.0"
+ "@webassemblyjs/wasm-gen" "1.9.0"
+ "@webassemblyjs/wasm-parser" "1.9.0"
+
+"@webassemblyjs/wasm-parser@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
+ integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/helper-api-error" "1.11.1"
+ "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+ "@webassemblyjs/ieee754" "1.11.1"
+ "@webassemblyjs/leb128" "1.11.1"
+ "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wasm-parser@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e"
+ integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/helper-api-error" "1.9.0"
+ "@webassemblyjs/helper-wasm-bytecode" "1.9.0"
+ "@webassemblyjs/ieee754" "1.9.0"
+ "@webassemblyjs/leb128" "1.9.0"
+ "@webassemblyjs/utf8" "1.9.0"
+
+"@webassemblyjs/wast-parser@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914"
+ integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/floating-point-hex-parser" "1.9.0"
+ "@webassemblyjs/helper-api-error" "1.9.0"
+ "@webassemblyjs/helper-code-frame" "1.9.0"
+ "@webassemblyjs/helper-fsm" "1.9.0"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
+ integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
+ dependencies:
+ "@webassemblyjs/ast" "1.11.1"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.9.0":
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899"
+ integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/wast-parser" "1.9.0"
+ "@xtuc/long" "4.2.2"
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
JSONStream@^1.0.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@@ -2342,6 +4394,14 @@ abab@^2.0.3, abab@^2.0.5:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==
+accepts@~1.3.5, accepts@~1.3.8:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
+ integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+ dependencies:
+ mime-types "~2.1.34"
+ negotiator "0.6.3"
+
ackee-tracker@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/ackee-tracker/-/ackee-tracker-5.1.0.tgz#6c41ea5357973347c7c67a26009053bcc0345def"
@@ -2357,12 +4417,17 @@ acorn-globals@^6.0.0:
acorn "^7.1.1"
acorn-walk "^7.1.1"
+acorn-import-assertions@^1.7.6:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
+ integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
+
acorn-jsx@^5.0.0, acorn-jsx@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
-acorn-walk@^7.1.1:
+acorn-walk@^7.1.1, acorn-walk@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
@@ -2372,12 +4437,17 @@ acorn-walk@^8.0.0, acorn-walk@^8.1.1:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
-acorn@^7.1.1:
+acorn@^6.4.1:
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
+ integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
+
+acorn@^7.1.1, acorn@^7.4.1:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-acorn@^8.0.0, acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.7.0:
+acorn@^8.0.0, acorn@^8.0.4, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
@@ -2387,6 +4457,19 @@ add-stream@^1.0.0:
resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa"
integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=
+address@^1.0.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
+ integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==
+
+adjust-sourcemap-loader@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99"
+ integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==
+ dependencies:
+ loader-utils "^2.0.0"
+ regex-parser "^2.2.11"
+
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -2402,7 +4485,40 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
-ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.6:
+airbnb-js-shims@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/airbnb-js-shims/-/airbnb-js-shims-2.2.1.tgz#db481102d682b98ed1daa4c5baa697a05ce5c040"
+ integrity sha512-wJNXPH66U2xjgo1Zwyjf9EydvJ2Si94+vSdk6EERcBfB2VZkeltpqIats0cqIZMLCXP3zcyaUKGYQeIBT6XjsQ==
+ dependencies:
+ array-includes "^3.0.3"
+ array.prototype.flat "^1.2.1"
+ array.prototype.flatmap "^1.2.1"
+ es5-shim "^4.5.13"
+ es6-shim "^0.35.5"
+ function.prototype.name "^1.1.0"
+ globalthis "^1.0.0"
+ object.entries "^1.1.0"
+ object.fromentries "^2.0.0 || ^1.0.0"
+ object.getownpropertydescriptors "^2.0.3"
+ object.values "^1.1.0"
+ promise.allsettled "^1.0.0"
+ promise.prototype.finally "^3.1.0"
+ string.prototype.matchall "^4.0.0 || ^3.0.1"
+ string.prototype.padend "^3.0.0"
+ string.prototype.padstart "^3.0.0"
+ symbol.prototype.description "^1.0.0"
+
+ajv-errors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
+ integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
+
+ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+ integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -2422,6 +4538,18 @@ ajv@^8.0.1:
require-from-string "^2.0.2"
uri-js "^4.2.2"
+ansi-align@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
+ integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
+ dependencies:
+ string-width "^4.1.0"
+
+ansi-colors@^3.0.0:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
+ integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
+
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@@ -2429,6 +4557,11 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
dependencies:
type-fest "^0.21.3"
+ansi-html-community@0.0.8, ansi-html-community@^0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
+ integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
+
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
@@ -2468,7 +4601,22 @@ ansi-styles@^6.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3"
integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==
-anymatch@^3.0.3, anymatch@~3.1.2:
+ansi-to-html@^0.6.11:
+ version "0.6.15"
+ resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.15.tgz#ac6ad4798a00f6aa045535d7f6a9cb9294eebea7"
+ integrity sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==
+ dependencies:
+ entities "^2.0.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+anymatch@^3.0.0, anymatch@^3.0.3, anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
@@ -2476,11 +4624,29 @@ anymatch@^3.0.3, anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
-aproba@^1.0.3:
+app-root-dir@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118"
+ integrity sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg=
+
+aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+"aproba@^1.0.3 || ^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
+ integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+
+are-we-there-yet@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
+ integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^3.6.0"
+
are-we-there-yet@~1.1.2:
version "1.1.7"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146"
@@ -2519,17 +4685,37 @@ aria-query@^5.0.0:
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c"
integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
array-find-index@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
array-ify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece"
integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=
-array-includes@^3.1.4:
+array-includes@^3.0.3, array-includes@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9"
integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==
@@ -2540,11 +4726,37 @@ array-includes@^3.1.4:
get-intrinsic "^1.1.1"
is-string "^1.0.7"
+array-union@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+ dependencies:
+ array-uniq "^1.0.1"
+
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
+array-uniq@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+ integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+array.prototype.flat@^1.2.1:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13"
+ integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+
array.prototype.flat@^1.2.5:
version "1.3.0"
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b"
@@ -2555,6 +4767,15 @@ array.prototype.flat@^1.2.5:
es-abstract "^1.19.2"
es-shim-unscopables "^1.0.0"
+array.prototype.flatmap@^1.2.1:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446"
+ integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==
+ dependencies:
+ call-bind "^1.0.0"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+
array.prototype.flatmap@^1.2.5:
version "1.3.0"
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz#a7e8ed4225f4788a70cd910abcf0791e76a5534f"
@@ -2565,16 +4786,62 @@ array.prototype.flatmap@^1.2.5:
es-abstract "^1.19.2"
es-shim-unscopables "^1.0.0"
+array.prototype.map@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.4.tgz#0d97b640cfdd036c1b41cfe706a5e699aa0711f2"
+ integrity sha512-Qds9QnX7A0qISY7JT5WuJO0NJPE9CMlC6JzHQfhpqAAQQzufVRoeH7EzUY5GcPTx72voG8LV/5eo+b8Qi8hmhA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+ es-array-method-boxes-properly "^1.0.0"
+ is-string "^1.0.7"
+
arrify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
+arrify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
+ integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
+
+asn1.js@^5.2.0:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
+ integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ safer-buffer "^2.1.0"
+
+assert@^1.1.1:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
+ integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==
+ dependencies:
+ object-assign "^4.1.1"
+ util "0.10.3"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
ast-types-flow@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0=
+ast-types@^0.14.2:
+ version "0.14.2"
+ resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd"
+ integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==
+ dependencies:
+ tslib "^2.0.1"
+
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@@ -2585,16 +4852,39 @@ astring@^1.6.0:
resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.1.tgz#a91c4afd4af3523e11f31242a3d5d9af62bb6cc6"
integrity sha512-Aj3mbwVzj7Vve4I/v2JYOPFkCGM2YS7OqQTNSxmUR+LECRpokuPgAYghePgr6SALDo5bD5DlfbSaYjOzGJZOLQ==
+async-each@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf"
+ integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==
+
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+at-least-node@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
+ integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
+
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+autoprefixer@^9.8.6:
+ version "9.8.8"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a"
+ integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==
+ dependencies:
+ browserslist "^4.12.0"
+ caniuse-lite "^1.0.30001109"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ picocolors "^0.2.1"
+ postcss "^7.0.32"
+ postcss-value-parser "^4.1.0"
+
axe-core@^4.3.5:
version "4.4.1"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
@@ -2619,6 +4909,29 @@ babel-jest@^27.5.1:
graceful-fs "^4.2.9"
slash "^3.0.0"
+babel-loader@^8.0.0:
+ version "8.2.4"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.2.4.tgz#95f5023c791b2e9e2ca6f67b0984f39c82ff384b"
+ integrity sha512-8dytA3gcvPPPv4Grjhnt8b5IIiTcq/zeXOPk4iTYI0SVXcsmuGg7JtBRDp8S9X+gJfhQ8ektjXZlDu1Bb33U8A==
+ dependencies:
+ find-cache-dir "^3.3.1"
+ loader-utils "^2.0.0"
+ make-dir "^3.1.0"
+ schema-utils "^2.6.5"
+
+babel-plugin-add-react-displayname@^0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz#339d4cddb7b65fd62d1df9db9fe04de134122bd5"
+ integrity sha1-M51M3be2X9YtHfnbn+BN4TQSK9U=
+
+babel-plugin-apply-mdx-type-prop@1.6.22:
+ version "1.6.22"
+ resolved "https://registry.yarnpkg.com/babel-plugin-apply-mdx-type-prop/-/babel-plugin-apply-mdx-type-prop-1.6.22.tgz#d216e8fd0de91de3f1478ef3231e05446bc8705b"
+ integrity sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "7.10.4"
+ "@mdx-js/util" "1.6.22"
+
babel-plugin-dynamic-import-node@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3"
@@ -2626,7 +4939,30 @@ babel-plugin-dynamic-import-node@^2.3.3:
dependencies:
object.assign "^4.1.0"
-babel-plugin-istanbul@^6.1.1:
+babel-plugin-emotion@^10.0.27:
+ version "10.2.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz#a1fe3503cff80abfd0bdda14abd2e8e57a79d17d"
+ integrity sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@emotion/hash" "0.8.0"
+ "@emotion/memoize" "0.7.4"
+ "@emotion/serialize" "^0.11.16"
+ babel-plugin-macros "^2.0.0"
+ babel-plugin-syntax-jsx "^6.18.0"
+ convert-source-map "^1.5.0"
+ escape-string-regexp "^1.0.5"
+ find-root "^1.1.0"
+ source-map "^0.5.7"
+
+babel-plugin-extract-import-names@1.6.22:
+ version "1.6.22"
+ resolved "https://registry.yarnpkg.com/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz#de5f9a28eb12f3eb2578bf74472204e66d1a13dc"
+ integrity sha512-yJ9BsJaISua7d8zNT7oRG1ZLBJCIdZ4PZqmH8qa9N5AK01ifk3fnkc98AXhtzE7UkfCsEumvoQWgoYLhOnJ7jQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "7.10.4"
+
+babel-plugin-istanbul@^6.0.0, babel-plugin-istanbul@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73"
integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==
@@ -2647,6 +4983,29 @@ babel-plugin-jest-hoist@^27.5.1:
"@types/babel__core" "^7.0.0"
"@types/babel__traverse" "^7.0.6"
+babel-plugin-macros@^2.0.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138"
+ integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==
+ dependencies:
+ "@babel/runtime" "^7.7.2"
+ cosmiconfig "^6.0.0"
+ resolve "^1.12.0"
+
+babel-plugin-macros@^3.0.1:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
+ integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+ cosmiconfig "^7.0.0"
+ resolve "^1.19.0"
+
+babel-plugin-named-exports-order@^0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-named-exports-order/-/babel-plugin-named-exports-order-0.0.2.tgz#ae14909521cf9606094a2048239d69847540cb09"
+ integrity sha512-OgOYHOLoRK+/mvXU9imKHlG6GkPLYrUCvFXG/CM93R/aNNO8pOOF4aS+S8CCHMDQoNSeiOYEZb/G6RwL95Jktw==
+
babel-plugin-polyfill-corejs2@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5"
@@ -2656,6 +5015,14 @@ babel-plugin-polyfill-corejs2@^0.3.0:
"@babel/helper-define-polyfill-provider" "^0.3.1"
semver "^6.1.1"
+babel-plugin-polyfill-corejs3@^0.1.0:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.7.tgz#80449d9d6f2274912e05d9e182b54816904befd0"
+ integrity sha512-u+gbS9bbPhZWEeyy1oR/YaaSpod/KDT07arZHb80aTpl8H5ZBq+uN1nN9/xtX7jQyfLdPfoqI4Rue/MQSWJquw==
+ dependencies:
+ "@babel/helper-define-polyfill-provider" "^0.1.5"
+ core-js-compat "^3.8.1"
+
babel-plugin-polyfill-corejs3@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72"
@@ -2671,6 +5038,20 @@ babel-plugin-polyfill-regenerator@^0.3.0:
dependencies:
"@babel/helper-define-polyfill-provider" "^0.3.1"
+babel-plugin-react-docgen@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.2.1.tgz#7cc8e2f94e8dc057a06e953162f0810e4e72257b"
+ integrity sha512-UQ0NmGHj/HAqi5Bew8WvNfCk8wSsmdgNd8ZdMjBCICtyCJCq9LiqgqvjCYe570/Wg7AQArSq1VQ60Dd/CHN7mQ==
+ dependencies:
+ ast-types "^0.14.2"
+ lodash "^4.17.15"
+ react-docgen "^5.0.0"
+
+babel-plugin-syntax-jsx@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+ integrity sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=
+
babel-preset-current-node-syntax@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b"
@@ -2697,6 +5078,11 @@ babel-preset-jest@^27.5.1:
babel-plugin-jest-hoist "^27.5.1"
babel-preset-current-node-syntax "^1.0.0"
+bail@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
+ integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
+
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
@@ -2712,16 +5098,58 @@ balanced-match@^2.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
-base64-js@^1.3.1:
+base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+better-opn@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/better-opn/-/better-opn-2.1.1.tgz#94a55b4695dc79288f31d7d0e5f658320759f7c6"
+ integrity sha512-kIPXZS5qwyKiX/HcRvDYfmBQUa8XP17I0mYZZ0y4UhpYOSvtsLHDYqmomS+Mj20aDvD3knEiQ0ecQy2nhio3yA==
+ dependencies:
+ open "^7.0.3"
+
+big-integer@^1.6.7:
+ version "1.6.51"
+ resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686"
+ integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==
+
+big.js@^5.2.2:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+ integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+binary-extensions@^1.0.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
+ integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==
+
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+bindings@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
+ integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
+ dependencies:
+ file-uri-to-path "1.0.0"
+
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@@ -2731,11 +5159,63 @@ bl@^4.0.3:
inherits "^2.0.4"
readable-stream "^3.4.0"
+bluebird@^3.3.5, bluebird@^3.5.5:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+ integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
+ integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
+
+bn.js@^5.0.0, bn.js@^5.1.1:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
+ integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
+
+body-parser@1.19.2:
+ version "1.19.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.2.tgz#4714ccd9c157d44797b8b5607d72c0b89952f26e"
+ integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==
+ dependencies:
+ bytes "3.1.2"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.2"
+ http-errors "1.8.1"
+ iconv-lite "0.4.24"
+ on-finished "~2.3.0"
+ qs "6.9.7"
+ raw-body "2.4.3"
+ type-is "~1.6.18"
+
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+boxen@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
+ integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==
+ dependencies:
+ ansi-align "^3.0.0"
+ camelcase "^6.2.0"
+ chalk "^4.1.0"
+ cli-boxes "^2.2.1"
+ string-width "^4.2.2"
+ type-fest "^0.20.2"
+ widest-line "^3.1.0"
+ wrap-ansi "^7.0.0"
+
+bplist-parser@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.1.1.tgz#d60d5dcc20cba6dc7e1f299b35d3e1f95dafbae6"
+ integrity sha512-2AEM0FXy8ZxVLBuqX0hqt1gDwcnz2zygEkQ6zaD5Wko/sB9paUNwlpawrFtKeHUAQUOzjVy9AO4oeonqIHKA9Q==
+ dependencies:
+ big-integer "^1.6.7"
+
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@@ -2744,6 +5224,22 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
+braces@^2.3.1, braces@^2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -2751,12 +5247,83 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
+brorand@^1.0.1, brorand@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+ integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browser-assert@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200"
+ integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==
+
browser-process-hrtime@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"
integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
-browserslist@^4.17.5, browserslist@^4.20.2:
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+ integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
+ integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
+ integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
+browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
+ integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
+ dependencies:
+ bn.js "^5.0.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3"
+ integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==
+ dependencies:
+ bn.js "^5.1.1"
+ browserify-rsa "^4.0.1"
+ create-hash "^1.2.0"
+ create-hmac "^1.1.7"
+ elliptic "^6.5.3"
+ inherits "^2.0.4"
+ parse-asn1 "^5.1.5"
+ readable-stream "^3.6.0"
+ safe-buffer "^5.2.0"
+
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ dependencies:
+ pako "~1.0.5"
+
+browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.17.5, browserslist@^4.19.1, browserslist@^4.20.2:
version "4.20.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88"
integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==
@@ -2779,6 +5346,20 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+ integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+ version "4.9.2"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+ integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@@ -2787,6 +5368,99 @@ buffer@^5.5.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+ integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+ integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
+
+bytes@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+ integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+c8@^7.6.0:
+ version "7.11.0"
+ resolved "https://registry.yarnpkg.com/c8/-/c8-7.11.0.tgz#b3ab4e9e03295a102c47ce11d4ef6d735d9a9ac9"
+ integrity sha512-XqPyj1uvlHMr+Y1IeRndC2X5P7iJzJlEJwBpCdBbq2JocXOgJfr+JVfJkyNMGROke5LfKrhSFXGFXnwnRJAUJw==
+ dependencies:
+ "@bcoe/v8-coverage" "^0.2.3"
+ "@istanbuljs/schema" "^0.1.2"
+ find-up "^5.0.0"
+ foreground-child "^2.0.0"
+ istanbul-lib-coverage "^3.0.1"
+ istanbul-lib-report "^3.0.0"
+ istanbul-reports "^3.0.2"
+ rimraf "^3.0.0"
+ test-exclude "^6.0.0"
+ v8-to-istanbul "^8.0.0"
+ yargs "^16.2.0"
+ yargs-parser "^20.2.7"
+
+cacache@^12.0.2:
+ version "12.0.4"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c"
+ integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==
+ dependencies:
+ bluebird "^3.5.5"
+ chownr "^1.1.1"
+ figgy-pudding "^3.5.1"
+ glob "^7.1.4"
+ graceful-fs "^4.1.15"
+ infer-owner "^1.0.3"
+ lru-cache "^5.1.1"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.3"
+ ssri "^6.0.1"
+ unique-filename "^1.1.1"
+ y18n "^4.0.0"
+
+cacache@^15.0.5:
+ version "15.3.0"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
+ integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
+ dependencies:
+ "@npmcli/fs" "^1.0.0"
+ "@npmcli/move-file" "^1.0.1"
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ glob "^7.1.4"
+ infer-owner "^1.0.4"
+ lru-cache "^6.0.0"
+ minipass "^3.1.1"
+ minipass-collect "^1.0.2"
+ minipass-flush "^1.0.5"
+ minipass-pipeline "^1.2.2"
+ mkdirp "^1.0.3"
+ p-map "^4.0.0"
+ promise-inflight "^1.0.1"
+ rimraf "^3.0.2"
+ ssri "^8.0.1"
+ tar "^6.0.2"
+ unique-filename "^1.1.1"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -2795,11 +5469,37 @@ call-bind@^1.0.0, call-bind@^1.0.2:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
+call-me-maybe@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
+ integrity sha1-JtII6onje1y95gJQoV8DHBak1ms=
+
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+camel-case@^4.1.1, camel-case@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a"
+ integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==
+ dependencies:
+ pascal-case "^3.1.2"
+ tslib "^2.0.3"
+
+camelcase-css@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
+ integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ integrity sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
camelcase-keys@^6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
@@ -2809,6 +5509,11 @@ camelcase-keys@^6.2.2:
map-obj "^4.0.0"
quick-lru "^4.0.1"
+camelcase@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+ integrity sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==
+
camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
@@ -2819,17 +5524,39 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
+caniuse-lite@^1.0.30001109:
+ version "1.0.30001322"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz#2e4c09d11e1e8f852767dab287069a8d0c29d623"
+ integrity sha512-neRmrmIrCGuMnxGSoh+x7zYtQFFgnSY2jaomjU56sCkTA6JINqQrxutF459JpWcWRajvoyn95sOXq4Pqrnyjew==
+
caniuse-lite@^1.0.30001283, caniuse-lite@^1.0.30001317:
version "1.0.30001332"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd"
integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw==
+capture-exit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
+ integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==
+ dependencies:
+ rsvp "^4.8.4"
+
+case-sensitive-paths-webpack-plugin@^2.3.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
+ integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==
+
+ccount@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
+ integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==
+
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
-chalk@^2.0.0, chalk@^2.4.2:
+chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2864,22 +5591,42 @@ character-entities-html4@^2.0.0:
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==
+character-entities-legacy@^1.0.0:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
+ integrity sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==
+
character-entities-legacy@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b"
integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
+character-entities@^1.0.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
+ integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
+
character-entities@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.1.tgz#98724833e1e27990dee0bd0f2b8a859c3476aac7"
integrity sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ==
+character-reference-invalid@^1.0.0:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
+ integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
+
character-reference-invalid@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9"
integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
-"chokidar@>=3.0.0 <4.0.0":
+charcodes@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4"
+ integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ==
+
+"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1, chokidar@^3.4.2:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -2894,26 +5641,97 @@ character-reference-invalid@^2.0.0:
optionalDependencies:
fsevents "~2.3.2"
+chokidar@^2.1.8:
+ version "2.1.8"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
+ integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.1"
+ braces "^2.3.2"
+ glob-parent "^3.1.0"
+ inherits "^2.0.3"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^3.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.2.1"
+ upath "^1.1.1"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+chownr@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
+ integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
+
+chrome-trace-event@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+ integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+ci-info@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
+ integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
+
ci-info@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2"
integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
cjs-module-lexer@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+clean-css@^4.2.3:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"
+ integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==
+ dependencies:
+ source-map "~0.6.0"
+
+clean-css@^5.2.2:
+ version "5.2.4"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.2.4.tgz#982b058f8581adb2ae062520808fb2429bd487a4"
+ integrity sha512-nKseG8wCzEuji/4yrgM/5cthL9oTDc5UOQyFMvW/Q53oP6gLH690o1NbuTh6Y18nujr7BxlsFuS7gXLnLzKJGg==
+ dependencies:
+ source-map "~0.6.0"
+
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+cli-boxes@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
+ integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
+
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -2921,6 +5739,15 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
+cli-table3@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8"
+ integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==
+ dependencies:
+ string-width "^4.2.0"
+ optionalDependencies:
+ colors "1.4.0"
+
cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
@@ -2946,6 +5773,15 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
+clone-deep@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+ integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+ dependencies:
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.2"
+ shallow-clone "^3.0.0"
+
clone-regexp@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
@@ -2953,6 +5789,16 @@ clone-regexp@^2.1.0:
dependencies:
is-regexp "^2.0.0"
+clsx@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
+ integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
+
+clsx@^1.0.4:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
+ integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -2963,11 +5809,24 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+collapse-white-space@^1.0.2:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287"
+ integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==
+
collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -3000,6 +5859,11 @@ color-string@^1.9.0:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
+color-support@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
+ integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
+
color@^4.2.1:
version "4.2.3"
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
@@ -3013,11 +5877,21 @@ colord@^2.9.2:
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.2.tgz#25e2bacbbaa65991422c07ea209e2089428effb1"
integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==
+colorette@^1.2.2:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
+ integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
+
colorette@^2.0.16:
version "2.0.16"
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
+colors@1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
+ integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
+
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -3025,6 +5899,11 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
+comma-separated-tokens@^1.0.0:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
+ integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
+
comma-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz#d4c25abb679b7751c880be623c1179780fe1dd98"
@@ -3035,7 +5914,17 @@ commander@8, commander@^8.3.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
-commander@^6.2.0:
+commander@^2.19.0, commander@^2.20.0:
+ version "2.20.3"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+ integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
+ integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+
+commander@^6.2.0, commander@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
@@ -3045,6 +5934,16 @@ commander@^7.2.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+common-path-prefix@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0"
+ integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+ integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
compare-func@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3"
@@ -3053,11 +5952,46 @@ compare-func@^2.0.0:
array-ify "^1.0.0"
dot-prop "^5.1.0"
+component-emitter@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
+ integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
+
+compressible@~2.0.16:
+ version "2.0.18"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
+ integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
+ dependencies:
+ mime-db ">= 1.43.0 < 2"
+
+compression@^1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
+ integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
+ dependencies:
+ accepts "~1.3.5"
+ bytes "3.0.0"
+ compressible "~2.0.16"
+ debug "2.6.9"
+ on-headers "~1.0.2"
+ safe-buffer "5.1.2"
+ vary "~1.1.2"
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+concat-stream@^1.5.0:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+ integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+ dependencies:
+ buffer-from "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
concat-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1"
@@ -3068,11 +6002,33 @@ concat-stream@^2.0.0:
readable-stream "^3.0.2"
typedarray "^0.0.6"
-console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+console-browserify@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
+ integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
+
+console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+ integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+content-disposition@0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+ integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+ dependencies:
+ safe-buffer "5.2.1"
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12:
version "5.0.13"
resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz#896885d63b914a70d4934b59d2fe7bde1832b28c"
@@ -3245,13 +6201,40 @@ conventional-recommended-bump@6.1.0:
meow "^8.0.0"
q "^1.5.1"
-convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
dependencies:
safe-buffer "~5.1.1"
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+
+cookie@0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
+ integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
+
+copy-concurrently@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+ integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+ dependencies:
+ aproba "^1.1.1"
+ fs-write-stream-atomic "^1.0.8"
+ iferr "^0.1.5"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.0"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
core-js-compat@^3.20.2, core-js-compat@^3.21.0:
version "3.22.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.22.0.tgz#7ce17ab57c378be2c717c7c8ed8f82a50a25b3e4"
@@ -3260,11 +6243,29 @@ core-js-compat@^3.20.2, core-js-compat@^3.21.0:
browserslist "^4.20.2"
semver "7.0.0"
+core-js-compat@^3.8.1:
+ version "3.21.1"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82"
+ integrity sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==
+ dependencies:
+ browserslist "^4.19.1"
+ semver "7.0.0"
+
core-js-pure@^3.20.2:
version "3.22.0"
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.22.0.tgz#0eaa54b6d1f4ebb4d19976bb4916dfad149a3747"
integrity sha512-ylOC9nVy0ak1N+fPIZj00umoZHgUVqmucklP5RT5N+vJof38klKn8Ze6KGyvchdClvEBr6LcQqJpI216LUMqYA==
+core-js-pure@^3.8.1:
+ version "3.21.1"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.21.1.tgz#8c4d1e78839f5f46208de7230cebfb72bc3bdb51"
+ integrity sha512-12VZfFIu+wyVbBebyHmRTuEE/tZrB4tJToWcwAMcsp3h4+sHR+fMJWbKpYiCRWlhFBq+KNyO8rIV9rTkeVmznQ==
+
+core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2:
+ version "3.21.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94"
+ integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==
+
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@@ -3278,6 +6279,17 @@ cosmiconfig-typescript-loader@^1.0.0:
cosmiconfig "^7"
ts-node "^10.7.0"
+cosmiconfig@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982"
+ integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==
+ dependencies:
+ "@types/parse-json" "^4.0.0"
+ import-fresh "^3.1.0"
+ parse-json "^5.0.0"
+ path-type "^4.0.0"
+ yaml "^1.7.2"
+
cosmiconfig@^7, cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
@@ -3289,19 +6301,79 @@ cosmiconfig@^7, cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
path-type "^4.0.0"
yaml "^1.10.0"
+cp-file@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd"
+ integrity sha512-0Cbj7gyvFVApzpK/uhCtQ/9kE9UnYpxMzaq5nQQC/Dh4iaj5fxp7iEFIullrYwzj8nf0qnsI1Qsx34hAeAebvw==
+ dependencies:
+ graceful-fs "^4.1.2"
+ make-dir "^3.0.0"
+ nested-error-stacks "^2.0.0"
+ p-event "^4.1.0"
+
+cpy@^8.1.2:
+ version "8.1.2"
+ resolved "https://registry.yarnpkg.com/cpy/-/cpy-8.1.2.tgz#e339ea54797ad23f8e3919a5cffd37bfc3f25935"
+ integrity sha512-dmC4mUesv0OYH2kNFEidtf/skUwv4zePmGeepjyyJ0qTo5+8KhA1o99oIAwVVLzQMAeDJml74d6wPPKb6EZUTg==
+ dependencies:
+ arrify "^2.0.1"
+ cp-file "^7.0.0"
+ globby "^9.2.0"
+ has-glob "^1.0.0"
+ junk "^3.1.0"
+ nested-error-stacks "^2.1.0"
+ p-all "^2.1.0"
+ p-filter "^2.1.0"
+ p-map "^3.0.0"
+
+create-ecdh@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
+ integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.5.3"
+
+create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+ integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ md5.js "^1.3.4"
+ ripemd160 "^2.0.1"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+ integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
-cross-fetch@^3.1.5:
- version "3.1.5"
- resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
- integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
dependencies:
- node-fetch "2.6.7"
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
-cross-spawn@^7.0.2, cross-spawn@^7.0.3:
+cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -3310,11 +6382,63 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
+crypto-browserify@^3.11.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+ integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+ randomfill "^1.0.3"
+
css-functions-list@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.0.1.tgz#1460df7fb584d1692c30b105151dbb988c8094f9"
integrity sha512-PriDuifDt4u4rkDgnqRCLnjfMatufLmWNfQnGCq34xZwpY3oabwhB9SqRBmuvWUgndbemCFlKqg+nO7C2q0SBw==
+css-loader@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645"
+ integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==
+ dependencies:
+ camelcase "^5.3.1"
+ cssesc "^3.0.0"
+ icss-utils "^4.1.1"
+ loader-utils "^1.2.3"
+ normalize-path "^3.0.0"
+ postcss "^7.0.32"
+ postcss-modules-extract-imports "^2.0.0"
+ postcss-modules-local-by-default "^3.0.2"
+ postcss-modules-scope "^2.2.0"
+ postcss-modules-values "^3.0.0"
+ postcss-value-parser "^4.1.0"
+ schema-utils "^2.7.0"
+ semver "^6.3.0"
+
+css-loader@^5.0.1:
+ version "5.2.7"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.7.tgz#9b9f111edf6fb2be5dc62525644cbc9c232064ae"
+ integrity sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==
+ dependencies:
+ icss-utils "^5.1.0"
+ loader-utils "^2.0.0"
+ postcss "^8.2.15"
+ postcss-modules-extract-imports "^3.0.0"
+ postcss-modules-local-by-default "^4.0.0"
+ postcss-modules-scope "^3.0.0"
+ postcss-modules-values "^4.0.0"
+ postcss-value-parser "^4.1.0"
+ schema-utils "^3.0.0"
+ semver "^7.3.5"
+
css-select@^4.1.3:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
@@ -3382,7 +6506,7 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
-csstype@^2.6.8:
+csstype@^2.5.7, csstype@^2.6.8:
version "2.6.20"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.20.tgz#9229c65ea0b260cf4d3d997cb06288e36a8d6dda"
integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA==
@@ -3399,6 +6523,11 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
+cyclist@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
+ integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
+
damerau-levenshtein@^1.0.7:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@@ -3423,6 +6552,13 @@ dateformat@^3.0.0:
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
+debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@@ -3430,14 +6566,7 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, d
dependencies:
ms "2.1.2"
-debug@^2.6.9:
- version "2.6.9"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
- integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
- dependencies:
- ms "2.0.0"
-
-debug@^3.2.7:
+debug@^3.0.0, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -3452,7 +6581,7 @@ decamelize-keys@^1.1.0:
decamelize "^1.1.0"
map-obj "^1.0.0"
-decamelize@^1.1.0, decamelize@^1.2.0:
+decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@@ -3496,11 +6625,37 @@ deep-is@^0.1.3, deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+deep-object-diff@^1.1.0:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.7.tgz#348b3246f426427dd633eaa50e1ed1fc2eafc7e4"
+ integrity sha512-QkgBca0mL08P6HiOjoqvmm6xOAl2W6CT2+34Ljhg0OeFan8cwlcdq8jrLKsBBuUFAZLsN5b6y491KdKEoSo9lg==
+
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
+default-browser-id@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-1.0.4.tgz#e59d09a5d157b828b876c26816e61c3d2a2c203a"
+ integrity sha1-5Z0JpdFXuCi4dsJoFuYcPSosIDo=
+ dependencies:
+ bplist-parser "^0.1.0"
+ meow "^3.1.0"
+ untildify "^2.0.0"
+
+define-lazy-prop@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f"
+ integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==
+
+define-properties@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
define-properties@^1.1.3:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1"
@@ -3509,6 +6664,28 @@ define-properties@^1.1.3:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -3519,11 +6696,36 @@ delegates@^1.0.0:
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
dequal@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
+des.js@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
+ integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+ integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detab@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/detab/-/detab-2.0.4.tgz#b927892069aff405fbb9a186fe97a44a92a94b43"
+ integrity sha512-8zdsQA5bIkoRECvCrNKPla84lyoR7DSAyf7p0YgXzBO9PDJx8KntPUay7NS6yp+KdxdVtiE5SpHKtbp2ZQyA9g==
+ dependencies:
+ repeat-string "^1.5.4"
+
detect-indent@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6"
@@ -3539,6 +6741,21 @@ detect-newline@^3.0.0, detect-newline@^3.1.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
+detect-package-manager@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/detect-package-manager/-/detect-package-manager-2.0.1.tgz#6b182e3ae5e1826752bfef1de9a7b828cffa50d8"
+ integrity sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==
+ dependencies:
+ execa "^5.1.1"
+
+detect-port@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/detect-port/-/detect-port-1.3.0.tgz#d9c40e9accadd4df5cac6a782aefd014d573d1f1"
+ integrity sha512-E+B1gzkl2gqxt1IhUzwjrxBKRqx1UzC3WLONHinn8S3T6lwV/agVCyitiFOsGJ/eYuEUBvD71MZHy3Pv1G9doQ==
+ dependencies:
+ address "^1.0.1"
+ debug "^2.6.0"
+
diff-sequences@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
@@ -3554,6 +6771,22 @@ diff@^5.0.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
+diffie-hellman@^5.0.0:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
+ integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+dir-glob@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4"
+ integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==
+ dependencies:
+ path-type "^3.0.0"
+
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -3580,6 +6813,13 @@ dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz#102ee5f25eacce09bdf1cfa5a298f86da473be4b"
integrity sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==
+dom-converter@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
+ integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==
+ dependencies:
+ utila "~0.4"
+
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -3589,6 +6829,16 @@ dom-serializer@^1.0.1:
domhandler "^4.2.0"
entities "^2.0.0"
+dom-walk@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84"
+ integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==
+
+domain-browser@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+ integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
domelementtype@^2.0.1, domelementtype@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
@@ -3601,14 +6851,14 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
-domhandler@^4.2.0, domhandler@^4.3.1:
+domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
dependencies:
domelementtype "^2.2.0"
-domutils@^2.8.0:
+domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
@@ -3617,6 +6867,14 @@ domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
+dot-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
+ integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==
+ dependencies:
+ no-case "^3.0.4"
+ tslib "^2.0.3"
+
dot-prop@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -3624,6 +6882,16 @@ dot-prop@^5.1.0:
dependencies:
is-obj "^2.0.0"
+dotenv-expand@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
+ integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
+
+dotenv@^8.0.0:
+ version "8.6.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
+ integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==
+
dotgitignore@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b"
@@ -3637,16 +6905,44 @@ duplexer@^0.1.2:
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
+duplexify@^3.4.2, duplexify@^3.6.0:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309"
+ integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
electron-to-chromium@^1.4.84:
version "1.4.111"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.111.tgz#897613f6504f3f17c9381c7499a635b413e4df4e"
integrity sha512-/s3+fwhKf1YK4k7btOImOzCQLpUjS6MaPf0ODTNuT4eTM1Bg4itBpLkydhOzJmpmH6Z9eXFyuuK5czsmzRzwtw==
+elliptic@^6.5.3:
+ version "6.5.4"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
+ integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
+ dependencies:
+ bn.js "^4.11.9"
+ brorand "^1.1.0"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.1"
+ inherits "^2.0.4"
+ minimalistic-assert "^1.0.1"
+ minimalistic-crypto-utils "^1.0.1"
+
emittery@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
@@ -3667,13 +6963,66 @@ emoji-regex@^9.2.2:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
-end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+emojis-list@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
+ integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
+
+emotion-theming@^10.0.27:
+ version "10.3.0"
+ resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.3.0.tgz#7f84d7099581d7ffe808aab5cd870e30843db72a"
+ integrity sha512-mXiD2Oj7N9b6+h/dC6oLf9hwxbtKHQjoIqtodEyL8CpkN4F3V4IK/BT4D0C7zSs4BBFOu4UlPJbvvBLa88SGEA==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ "@emotion/weak-memoize" "0.2.5"
+ hoist-non-react-statics "^3.3.0"
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
+endent@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/endent/-/endent-2.1.0.tgz#5aaba698fb569e5e18e69e1ff7a28ff35373cd88"
+ integrity sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==
+ dependencies:
+ dedent "^0.7.0"
+ fast-json-parse "^1.0.3"
+ objectorarray "^1.0.5"
+
+enhanced-resolve@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec"
+ integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.5.0"
+ tapable "^1.0.0"
+
+enhanced-resolve@^5.9.2:
+ version "5.9.2"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9"
+ integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
+enhanced-resolve@^5.9.3:
+ version "5.9.3"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
+ integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
+ dependencies:
+ graceful-fs "^4.2.4"
+ tapable "^2.2.0"
+
entities@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
@@ -3684,13 +7033,53 @@ entities@^3.0.1:
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
-error-ex@^1.3.1:
+errno@^0.1.3, errno@~0.1.7:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
+ integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
+ dependencies:
+ prr "~1.0.1"
+
+error-ex@^1.2.0, error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
dependencies:
is-arrayish "^0.2.1"
+error-stack-parser@^2.0.6:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.7.tgz#b0c6e2ce27d0495cf78ad98715e0cad1219abb57"
+ integrity sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==
+ dependencies:
+ stackframe "^1.1.1"
+
+es-abstract@^1.19.0:
+ version "1.19.2"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.2.tgz#8f7b696d8f15b167ae3640b4060670f3d054143f"
+ integrity sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w==
+ dependencies:
+ call-bind "^1.0.2"
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ get-intrinsic "^1.1.1"
+ get-symbol-description "^1.0.0"
+ has "^1.0.3"
+ has-symbols "^1.0.3"
+ internal-slot "^1.0.3"
+ is-callable "^1.2.4"
+ is-negative-zero "^2.0.2"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.1"
+ is-string "^1.0.7"
+ is-weakref "^1.0.2"
+ object-inspect "^1.12.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.2"
+ string.prototype.trimend "^1.0.4"
+ string.prototype.trimstart "^1.0.4"
+ unbox-primitive "^1.0.1"
+
es-abstract@^1.19.1, es-abstract@^1.19.2:
version "1.19.5"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.5.tgz#a2cb01eb87f724e815b278b0dd0d00f36ca9a7f1"
@@ -3717,6 +7106,30 @@ es-abstract@^1.19.1, es-abstract@^1.19.2:
string.prototype.trimstart "^1.0.4"
unbox-primitive "^1.0.1"
+es-array-method-boxes-properly@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
+ integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==
+
+es-get-iterator@^1.0.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7"
+ integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.0"
+ has-symbols "^1.0.1"
+ is-arguments "^1.1.0"
+ is-map "^2.0.2"
+ is-set "^2.0.2"
+ is-string "^1.0.5"
+ isarray "^2.0.5"
+
+es-module-lexer@^0.9.0:
+ version "0.9.3"
+ resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19"
+ integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==
+
es-shim-unscopables@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241"
@@ -3733,11 +7146,26 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
+es5-shim@^4.5.13:
+ version "4.6.5"
+ resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.6.5.tgz#2124bb073b7cede2ed23b122a1fd87bb7b0bb724"
+ integrity sha512-vfQ4UAai8szn0sAubCy97xnZ4sJVDD1gt/Grn736hg8D7540wemIb1YPrYZSTqlM2H69EQX1or4HU/tSwRTI3w==
+
+es6-shim@^0.35.5:
+ version "0.35.6"
+ resolved "https://registry.yarnpkg.com/es6-shim/-/es6-shim-0.35.6.tgz#d10578301a83af2de58b9eadb7c2c9945f7388a0"
+ integrity sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA==
+
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -3895,6 +7323,15 @@ eslint-plugin-react@7.29.1:
semver "^6.3.0"
string.prototype.matchall "^4.0.6"
+eslint-plugin-storybook@^0.5.12:
+ version "0.5.12"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-0.5.12.tgz#b84d38400b91a9abdf15cd2c81644bff27861a96"
+ integrity sha512-ojuNKnrZFrQpm5N5Lp8UR0VEn4HtLjlNn6nxQAYlmTsEXNigtId1XPuMbXAsvFcEmv3RTb5l+9tZgkhSURfACg==
+ dependencies:
+ "@storybook/csf" "^0.0.1"
+ "@typescript-eslint/experimental-utils" "^5.3.0"
+ requireindex "^1.1.0"
+
eslint-plugin-testing-library@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.3.1.tgz#7638ee79cc86fd8bb57d671af35a1cbaa77e9548"
@@ -3902,7 +7339,7 @@ eslint-plugin-testing-library@^5.3.1:
dependencies:
"@typescript-eslint/utils" "^5.13.0"
-eslint-scope@^5.1.1:
+eslint-scope@5.1.1, eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -3910,6 +7347,14 @@ eslint-scope@^5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
+eslint-scope@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
+ integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
eslint-scope@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642"
@@ -3997,7 +7442,7 @@ esquery@^1.4.0:
dependencies:
estraverse "^5.1.0"
-esrecurse@^4.3.0:
+esrecurse@^4.1.0, esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
@@ -4014,6 +7459,15 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+estree-to-babel@^3.1.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/estree-to-babel/-/estree-to-babel-3.2.1.tgz#82e78315275c3ca74475fdc8ac1a5103c8a75bf5"
+ integrity sha512-YNF+mZ/Wu2FU/gvmzuWtYc8rloubL7wfXCTgouFrnjGVXPA/EeYYA7pupXWrb3Iv1cTBeSSxxJIbK23l4MRNqg==
+ dependencies:
+ "@babel/traverse" "^7.1.6"
+ "@babel/types" "^7.2.0"
+ c8 "^7.6.0"
+
estree-util-attach-comments@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-2.0.0.tgz#2c06d484dfcf841b5946bcb84d5412cbcd544e22"
@@ -4058,6 +7512,42 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+events@^3.0.0, events@^3.2.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+ integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+exec-sh@^0.3.2:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc"
+ integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
execa@^5.0.0, execa@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -4085,6 +7575,19 @@ exit@^0.1.2:
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
@@ -4100,21 +7603,93 @@ expect@^27.5.1:
jest-matcher-utils "^27.5.1"
jest-message-util "^27.5.1"
+express@^4.17.1:
+ version "4.17.3"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1"
+ integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==
+ dependencies:
+ accepts "~1.3.8"
+ array-flatten "1.1.1"
+ body-parser "1.19.2"
+ content-disposition "0.5.4"
+ content-type "~1.0.4"
+ cookie "0.4.2"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~1.1.2"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "~1.1.2"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.7"
+ qs "6.9.7"
+ range-parser "~1.2.1"
+ safe-buffer "5.2.1"
+ send "0.17.2"
+ serve-static "1.14.2"
+ setprototypeof "1.2.0"
+ statuses "~1.5.0"
+ type-is "~1.6.18"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
extend@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
-extract-files@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a"
- integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==
-
-fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+fast-deep-equal@^3.0.0, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+fast-glob@^2.2.6:
+ version "2.2.7"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
+ integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==
+ dependencies:
+ "@mrmlnc/readdir-enhanced" "^2.2.1"
+ "@nodelib/fs.stat" "^1.1.2"
+ glob-parent "^3.1.0"
+ is-glob "^4.0.0"
+ merge2 "^1.2.3"
+ micromatch "^3.1.10"
+
fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9:
version "3.2.11"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9"
@@ -4126,6 +7701,11 @@ fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
+fast-json-parse@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d"
+ integrity sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==
+
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@@ -4148,6 +7728,13 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
+fault@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
+ integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
+ dependencies:
+ format "^0.2.0"
+
fb-watchman@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
@@ -4162,6 +7749,16 @@ feed@^4.2.2:
dependencies:
xml-js "^1.6.11"
+fetch-retry@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-5.0.2.tgz#4c55663a7c056cb45f182394e479464f0ff8f3e3"
+ integrity sha512-57Hmu+1kc6pKFUGVIobT7qw3NeAzY/uNN26bSevERLVvf6VGFR/ooDCOFBHMNDgAxBiU2YJq1D0vFzc6U1DcPw==
+
+figgy-pudding@^3.5.1:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
+ integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
+
figures@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@@ -4176,6 +7773,38 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
+file-loader@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
+ integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
+ dependencies:
+ loader-utils "^2.0.0"
+ schema-utils "^3.0.0"
+
+file-system-cache@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"
+ integrity sha1-hCWbNqK7uNPW6xAh0xMv/mTP/08=
+ dependencies:
+ bluebird "^3.3.5"
+ fs-extra "^0.30.0"
+ ramda "^0.21.0"
+
+file-uri-to-path@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
+ integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@@ -4183,6 +7812,50 @@ fill-range@^7.0.1:
dependencies:
to-regex-range "^5.0.1"
+finalhandler@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
+ integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.3"
+ statuses "~1.5.0"
+ unpipe "~1.0.0"
+
+find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
+ integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^2.0.0"
+ pkg-dir "^3.0.0"
+
+find-cache-dir@^3.3.1:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b"
+ integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^3.0.2"
+ pkg-dir "^4.1.0"
+
+find-root@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
+ integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
find-up@^2.0.0, find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
@@ -4226,6 +7899,66 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
+flush-write-stream@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
+ integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.3.6"
+
+focus-lock@^0.8.0:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.8.1.tgz#bb36968abf77a2063fa173cb6c47b12ac8599d33"
+ integrity sha512-/LFZOIo82WDsyyv7h7oc0MJF9ACOvDRdx9rWPZ2pgMfNWu/z8hQDBtOchuB/0BVLmuFOZjV02YwUVzNsWx/EzA==
+ dependencies:
+ tslib "^1.9.3"
+
+for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+foreground-child@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
+ integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==
+ dependencies:
+ cross-spawn "^7.0.0"
+ signal-exit "^3.0.2"
+
+fork-ts-checker-webpack-plugin@^4.1.6:
+ version "4.1.6"
+ resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5"
+ integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw==
+ dependencies:
+ "@babel/code-frame" "^7.5.5"
+ chalk "^2.4.1"
+ micromatch "^3.1.10"
+ minimatch "^3.0.4"
+ semver "^5.6.0"
+ tapable "^1.0.0"
+ worker-rpc "^0.1.0"
+
+fork-ts-checker-webpack-plugin@^6.0.4:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz#0282b335fa495a97e167f69018f566ea7d2a2b5e"
+ integrity sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw==
+ dependencies:
+ "@babel/code-frame" "^7.8.3"
+ "@types/json-schema" "^7.0.5"
+ chalk "^4.1.0"
+ chokidar "^3.4.2"
+ cosmiconfig "^6.0.0"
+ deepmerge "^4.2.2"
+ fs-extra "^9.0.0"
+ glob "^7.1.6"
+ memfs "^3.1.2"
+ minimatch "^3.0.4"
+ schema-utils "2.7.0"
+ semver "^7.3.2"
+ tapable "^1.0.0"
+
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
@@ -4235,6 +7968,36 @@ form-data@^3.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
+format@^0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
+ integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=
+
+forwarded@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+ integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+from2@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+ integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+
fs-access@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a"
@@ -4256,12 +8019,63 @@ fs-extra@10, fs-extra@^10.0.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
+fs-extra@^0.30.0:
+ version "0.30.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
+ integrity sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+ path-is-absolute "^1.0.0"
+ rimraf "^2.2.8"
+
+fs-extra@^9.0.0, fs-extra@^9.0.1:
+ version "9.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
+ integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
+ dependencies:
+ at-least-node "^1.0.0"
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
+fs-minipass@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
+ integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
+ dependencies:
+ minipass "^3.0.0"
+
+fs-monkey@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3"
+ integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==
+
+fs-write-stream-atomic@^1.0.8:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+ integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+ dependencies:
+ graceful-fs "^4.1.2"
+ iferr "^0.1.5"
+ imurmurhash "^0.1.4"
+ readable-stream "1 || 2"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-fsevents@^2.3.2, fsevents@~2.3.2:
+fsevents@^1.2.7:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
+ integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==
+ dependencies:
+ bindings "^1.5.0"
+ nan "^2.12.1"
+
+fsevents@^2.1.2, fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -4271,6 +8085,16 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+function.prototype.name@^1.1.0:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621"
+ integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.0"
+ functions-have-names "^1.2.2"
+
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
@@ -4281,6 +8105,21 @@ functions-have-names@^1.2.2:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.2.tgz#98d93991c39da9361f8e50b337c4f6e41f120e21"
integrity sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==
+gauge@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395"
+ integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==
+ dependencies:
+ aproba "^1.0.3 || ^2.0.0"
+ color-support "^1.1.2"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.1"
+ object-assign "^4.1.1"
+ signal-exit "^3.0.0"
+ string-width "^4.2.3"
+ strip-ansi "^6.0.1"
+ wide-align "^1.1.2"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -4295,7 +8134,7 @@ gauge@~2.7.3:
strip-ansi "^3.0.1"
wide-align "^1.1.0"
-gensync@^1.0.0-beta.2:
+gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
@@ -4329,11 +8168,23 @@ get-pkg-repo@^4.0.0:
through2 "^2.0.0"
yargs "^16.2.0"
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+ integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
+
get-stdin@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ dependencies:
+ pump "^3.0.0"
+
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@@ -4347,6 +8198,11 @@ get-symbol-description@^1.0.0:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
git-raw-commits@^2.0.0, git-raw-commits@^2.0.8:
version "2.0.11"
resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.11.tgz#bc3576638071d18655e1cc60d7f524920008d723"
@@ -4386,6 +8242,19 @@ github-from-package@0.0.0:
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
+github-slugger@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e"
+ integrity sha512-w0dzqw/nt51xMVmlaV1+JRzN+oCa1KfcgGEWhxUG16wbdA+Xnt/yoFO8Z8x/V82ZcZ0wy6ln9QDup5avbhiDhQ==
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -4400,6 +8269,23 @@ glob-parent@^6.0.1:
dependencies:
is-glob "^4.0.3"
+glob-promise@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20"
+ integrity sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==
+ dependencies:
+ "@types/glob" "*"
+
+glob-to-regexp@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
+ integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
+
+glob-to-regexp@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+ integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
glob@7.1.7:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
@@ -4447,6 +8333,14 @@ global-prefix@^3.0.0:
kind-of "^6.0.2"
which "^1.3.1"
+global@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
+ integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
+ dependencies:
+ min-document "^2.19.0"
+ process "^0.11.10"
+
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@@ -4459,7 +8353,14 @@ globals@^13.6.0, globals@^13.9.0:
dependencies:
type-fest "^0.20.2"
-globby@^11.0.4, globby@^11.1.0:
+globalthis@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.2.tgz#2a235d34f4d8036219f7e34929b5de9e18166b8b"
+ integrity sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==
+ dependencies:
+ define-properties "^1.1.3"
+
+globby@^11.0.2, globby@^11.0.4, globby@^11.1.0:
version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
@@ -4471,25 +8372,35 @@ globby@^11.0.4, globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
+globby@^9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d"
+ integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==
+ dependencies:
+ "@types/glob" "^7.1.1"
+ array-union "^1.0.2"
+ dir-glob "^2.2.2"
+ fast-glob "^2.2.6"
+ glob "^7.1.3"
+ ignore "^4.0.3"
+ pify "^4.0.1"
+ slash "^2.0.0"
+
globjoin@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43"
integrity sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.9, graceful-fs@^4.2.4:
+ version "4.2.9"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
+ integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
+
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9:
version "4.2.10"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
-graphql-request@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-4.2.0.tgz#063377bc2dd29cc46aed3fddcc65fe97b805ba81"
- integrity sha512-uFeMyhhl8ss4LFgjlfPeAn2pqYw+CJto+cjj71uaBYIMMK2jPIqgHm5KEFxUk0YDD41A8Bq31a2b4G2WJBlp2Q==
- dependencies:
- cross-fetch "^3.1.5"
- extract-files "^9.0.0"
- form-data "^3.0.0"
-
graphql@^16.1.0:
version "16.3.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.3.0.tgz#a91e24d10babf9e60c706919bb182b53ccdffc05"
@@ -4534,6 +8445,13 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+has-glob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-glob/-/has-glob-1.0.0.tgz#9aaa9eedbffb1ba3990a7b0010fb678ee0081207"
+ integrity sha1-mqqe7b/7G6OZCnsAEPtnjuAIEgc=
+ dependencies:
+ is-glob "^3.0.0"
+
has-property-descriptors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
@@ -4553,11 +8471,42 @@ has-tostringtag@^1.0.0:
dependencies:
has-symbols "^1.0.2"
-has-unicode@^2.0.0:
+has-unicode@^2.0.0, has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
@@ -4565,6 +8514,69 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
+hash-base@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
+ integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
+ dependencies:
+ inherits "^2.0.4"
+ readable-stream "^3.6.0"
+ safe-buffer "^5.2.0"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+ integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.1"
+
+hast-to-hyperscript@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/hast-to-hyperscript/-/hast-to-hyperscript-9.0.1.tgz#9b67fd188e4c81e8ad66f803855334173920218d"
+ integrity sha512-zQgLKqF+O2F72S1aa4y2ivxzSlko3MAvxkwG8ehGmNiqd98BIN3JM1rAJPmplEyLmGLO2QZYJtIneOSZ2YbJuA==
+ dependencies:
+ "@types/unist" "^2.0.3"
+ comma-separated-tokens "^1.0.0"
+ property-information "^5.3.0"
+ space-separated-tokens "^1.0.0"
+ style-to-object "^0.3.0"
+ unist-util-is "^4.0.0"
+ web-namespaces "^1.0.0"
+
+hast-util-from-parse5@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-6.0.1.tgz#554e34abdeea25ac76f5bd950a1f0180e0b3bc2a"
+ integrity sha512-jeJUWiN5pSxW12Rh01smtVkZgZr33wBokLzKLwinYOUfSzm1Nl/c3GUGebDyOKjdsRgMvoVbV0VpAcpjF4NrJA==
+ dependencies:
+ "@types/parse5" "^5.0.0"
+ hastscript "^6.0.0"
+ property-information "^5.0.0"
+ vfile "^4.0.0"
+ vfile-location "^3.2.0"
+ web-namespaces "^1.0.0"
+
+hast-util-parse-selector@^2.0.0:
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz#d57c23f4da16ae3c63b3b6ca4616683313499c3a"
+ integrity sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==
+
+hast-util-raw@6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-6.0.1.tgz#973b15930b7529a7b66984c98148b46526885977"
+ integrity sha512-ZMuiYA+UF7BXBtsTBNcLBF5HzXzkyE6MLzJnL605LKE8GJylNjGc4jjxazAHUtcwT5/CEt6afRKViYB4X66dig==
+ dependencies:
+ "@types/hast" "^2.0.0"
+ hast-util-from-parse5 "^6.0.0"
+ hast-util-to-parse5 "^6.0.0"
+ html-void-elements "^1.0.0"
+ parse5 "^6.0.0"
+ unist-util-position "^3.0.0"
+ vfile "^4.0.0"
+ web-namespaces "^1.0.0"
+ xtend "^4.0.0"
+ zwitch "^1.0.0"
+
hast-util-to-estree@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-2.0.2.tgz#79c5bf588915610b3f0d47ca83a74dc0269c7dc2"
@@ -4585,11 +8597,66 @@ hast-util-to-estree@^2.0.0:
unist-util-position "^4.0.0"
zwitch "^2.0.0"
+hast-util-to-parse5@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-6.0.0.tgz#1ec44650b631d72952066cea9b1445df699f8479"
+ integrity sha512-Lu5m6Lgm/fWuz8eWnrKezHtVY83JeRGaNQ2kn9aJgqaxvVkFCZQBEhgodZUDUvoodgyROHDb3r5IxAEdl6suJQ==
+ dependencies:
+ hast-to-hyperscript "^9.0.0"
+ property-information "^5.0.0"
+ web-namespaces "^1.0.0"
+ xtend "^4.0.0"
+ zwitch "^1.0.0"
+
hast-util-whitespace@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz#4fc1086467cc1ef5ba20673cb6b03cec3a970f1c"
integrity sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg==
+hastscript@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
+ integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==
+ dependencies:
+ "@types/hast" "^2.0.0"
+ comma-separated-tokens "^1.0.0"
+ hast-util-parse-selector "^2.0.0"
+ property-information "^5.0.0"
+ space-separated-tokens "^1.0.0"
+
+he@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+ integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
+highlight.js@^10.4.1, highlight.js@~10.7.0:
+ version "10.7.3"
+ resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
+ integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+
+history@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08"
+ integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==
+ dependencies:
+ "@babel/runtime" "^7.7.6"
+
+history@^5.2.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/history/-/history-5.3.0.tgz#1548abaa245ba47992f063a0783db91ef201c73b"
+ integrity sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==
+ dependencies:
+ "@babel/runtime" "^7.7.6"
+
+hmac-drbg@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -4616,16 +8683,104 @@ html-encoding-sniffer@^2.0.1:
dependencies:
whatwg-encoding "^1.0.5"
+html-entities@^2.1.0:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
+ integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
+
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+html-minifier-terser@^5.0.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054"
+ integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==
+ dependencies:
+ camel-case "^4.1.1"
+ clean-css "^4.2.3"
+ commander "^4.1.1"
+ he "^1.2.0"
+ param-case "^3.0.3"
+ relateurl "^0.2.7"
+ terser "^4.6.3"
+
+html-minifier-terser@^6.0.2:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab"
+ integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==
+ dependencies:
+ camel-case "^4.1.2"
+ clean-css "^5.2.2"
+ commander "^8.3.0"
+ he "^1.2.0"
+ param-case "^3.0.4"
+ relateurl "^0.2.7"
+ terser "^5.10.0"
+
+html-tags@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
+ integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
+
html-tags@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961"
integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==
+html-void-elements@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-1.0.5.tgz#ce9159494e86d95e45795b166c2021c2cfca4483"
+ integrity sha512-uE/TxKuyNIcx44cIWnjr/rfIATDH7ZaOMmstu0CwhFG1Dunhlp4OC6/NMbhiwoq5BpW0ubi303qnEk/PZj614w==
+
+html-webpack-plugin@^4.0.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.5.2.tgz#76fc83fa1a0f12dd5f7da0404a54e2699666bc12"
+ integrity sha512-q5oYdzjKUIPQVjOosjgvCHQOv9Ett9CYYHlgvJeXG0qQvdSojnBq4vAdQBwn1+yGveAwHCoe/rMR86ozX3+c2A==
+ dependencies:
+ "@types/html-minifier-terser" "^5.0.0"
+ "@types/tapable" "^1.0.5"
+ "@types/webpack" "^4.41.8"
+ html-minifier-terser "^5.0.1"
+ loader-utils "^1.2.3"
+ lodash "^4.17.20"
+ pretty-error "^2.1.1"
+ tapable "^1.1.3"
+ util.promisify "1.0.0"
+
+html-webpack-plugin@^5.0.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz#c3911936f57681c1f9f4d8b68c158cd9dfe52f50"
+ integrity sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==
+ dependencies:
+ "@types/html-minifier-terser" "^6.0.0"
+ html-minifier-terser "^6.0.2"
+ lodash "^4.17.21"
+ pretty-error "^4.0.0"
+ tapable "^2.0.0"
+
+htmlparser2@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
+ integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
+ dependencies:
+ domelementtype "^2.0.1"
+ domhandler "^4.0.0"
+ domutils "^2.5.2"
+ entities "^2.0.0"
+
+http-errors@1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
+ integrity sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.4"
+ setprototypeof "1.2.0"
+ statuses ">= 1.5.0 < 2"
+ toidentifier "1.0.1"
+
http-proxy-agent@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a"
@@ -4635,6 +8790,11 @@ http-proxy-agent@^4.0.1:
agent-base "6"
debug "4"
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+ integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@@ -4660,22 +8820,51 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-ieee754@^1.1.13:
+icss-utils@^4.0.0, icss-utils@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
+ integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
+ dependencies:
+ postcss "^7.0.14"
+
+icss-utils@^5.0.0, icss-utils@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
+ integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
+
+ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+iferr@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+ integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+
+ignore@^4.0.3:
+ version "4.0.6"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
+ integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
+
ignore@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==
+image-size@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.1.tgz#86d6cfc2b1d19eab5d2b368d4b9194d9e48541c5"
+ integrity sha512-VAwkvNSNGClRw9mDHhc5Efax8PLlsOGcUTh0T/LIriC8vPA3U5PdqXWqkz406MoYHMKW8Uf9gWr05T/rYB44kQ==
+ dependencies:
+ queue "6.0.2"
+
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
-import-fresh@^3.0.0, import-fresh@^3.2.1:
+import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -4701,11 +8890,23 @@ imurmurhash@^0.1.4:
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=
+ dependencies:
+ repeating "^2.0.0"
+
indent-string@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
+infer-owner@^1.0.3, infer-owner@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467"
+ integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==
+
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -4714,11 +8915,21 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
+inherits@2, inherits@2.0.4, inherits@^2.0.0, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+ integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+inherits@2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
@@ -4738,6 +8949,11 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
+interpret@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
+ integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+
intl-messageformat@9.12.0:
version "9.12.0"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-9.12.0.tgz#b69d042fa7db229e799eaf3afb09f8ceadd306e7"
@@ -4748,11 +8964,53 @@ intl-messageformat@9.12.0:
"@formatjs/icu-messageformat-parser" "2.0.19"
tslib "^2.1.0"
+ip@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+ integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
+ipaddr.js@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-absolute-url@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+ integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-alphabetical@1.0.4, is-alphabetical@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
+ integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==
+
is-alphabetical@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b"
integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==
+is-alphanumerical@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz#7eb9a2431f855f6b1ef1a78e326df515696c4dbf"
+ integrity sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==
+ dependencies:
+ is-alphabetical "^1.0.0"
+ is-decimal "^1.0.0"
+
is-alphanumerical@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875"
@@ -4761,6 +9019,14 @@ is-alphanumerical@^2.0.0:
is-alphabetical "^2.0.0"
is-decimal "^2.0.0"
+is-arguments@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
+ integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
is-arrayish@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -4778,6 +9044,13 @@ is-bigint@^1.0.1:
dependencies:
has-bigints "^1.0.1"
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+ dependencies:
+ binary-extensions "^1.0.0"
+
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -4793,6 +9066,11 @@ is-boolean-object@^1.1.0:
call-bind "^1.0.2"
has-tostringtag "^1.0.0"
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
is-buffer@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191"
@@ -4803,6 +9081,13 @@ is-callable@^1.1.4, is-callable@^1.2.4:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
+is-ci@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
+ integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
+ dependencies:
+ ci-info "^2.0.0"
+
is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.7.0, is-core-module@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
@@ -4810,6 +9095,20 @@ is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.7.0, is-core-mod
dependencies:
has "^1.0.3"
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
is-date-object@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
@@ -4817,16 +9116,69 @@ is-date-object@^1.0.1:
dependencies:
has-tostringtag "^1.0.0"
+is-decimal@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
+ integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
+
is-decimal@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7"
integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==
-is-extglob@^2.1.1:
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-docker@^2.0.0, is-docker@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+ integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
+
+is-dom@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-dom/-/is-dom-1.1.0.tgz#af1fced292742443bb59ca3f76ab5e80907b4e8a"
+ integrity sha512-u82f6mvhYxRPKpw8V1N0W8ce1xXwOrQtgGcxl6UCL5zBmZu3is/18K0rR7uFCnMDuAsS/3W54mGL4vsaFUQlEQ==
+ dependencies:
+ is-object "^1.0.1"
+ is-window "^1.0.2"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+is-finite@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
+ integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
+
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
@@ -4844,11 +9196,23 @@ is-fullwidth-code-point@^4.0.0:
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88"
integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==
+is-function@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.2.tgz#4f097f30abf6efadac9833b17ca5dc03f8144e08"
+ integrity sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==
+
is-generator-fn@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
+is-glob@^3.0.0, is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+ dependencies:
+ is-extglob "^2.1.0"
+
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
@@ -4856,11 +9220,21 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
+is-hexadecimal@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
+ integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
+
is-hexadecimal@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027"
integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==
+is-map@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
+ integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==
+
is-negative-zero@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
@@ -4873,6 +9247,13 @@ is-number-object@^1.0.4:
dependencies:
has-tostringtag "^1.0.0"
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
@@ -4883,21 +9264,38 @@ is-obj@^2.0.0:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
+is-object@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf"
+ integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==
+
is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+is-plain-obj@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
+ integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
+
is-plain-obj@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.0.0.tgz#06c0999fd7574edf5a906ba5644ad0feb3a84d22"
integrity sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw==
-is-plain-object@^5.0.0:
+is-plain-object@5.0.0, is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
+is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
@@ -4910,7 +9308,7 @@ is-reference@^3.0.0:
dependencies:
"@types/estree" "*"
-is-regex@^1.1.4:
+is-regex@^1.1.2, is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@@ -4923,13 +9321,23 @@ is-regexp@^2.0.0:
resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-2.1.0.tgz#cd734a56864e23b956bf4e7c66c396a4c0b22c2d"
integrity sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==
-is-shared-array-buffer@^1.0.2:
+is-set@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec"
+ integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==
+
+is-shared-array-buffer@^1.0.1, is-shared-array-buffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79"
integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==
dependencies:
call-bind "^1.0.2"
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@@ -4961,6 +9369,11 @@ is-typedarray@^1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+ integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
+
is-weakref@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2"
@@ -4968,17 +9381,79 @@ is-weakref@^1.0.2:
dependencies:
call-bind "^1.0.2"
-isarray@~1.0.0:
+is-whitespace-character@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz#0858edd94a95594c7c9dd0b5c174ec6e45ee4aa7"
+ integrity sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==
+
+is-window@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-window/-/is-window-1.0.2.tgz#2c896ca53db97de45d3c33133a65d8c9f563480d"
+ integrity sha1-LIlspT25feRdPDMTOmXYyfVjSA0=
+
+is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-word-character@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.4.tgz#ce0e73216f98599060592f62ff31354ddbeb0230"
+ integrity sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+ integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+is-wsl@^2.1.1, is-wsl@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+ integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+ dependencies:
+ is-docker "^2.0.0"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+isarray@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
-istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isobject@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
+ integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
+
+isomorphic-unfetch@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f"
+ integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==
+ dependencies:
+ node-fetch "^2.6.1"
+ unfetch "^4.2.0"
+
+istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.1, istanbul-lib-coverage@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
@@ -5012,7 +9487,7 @@ istanbul-lib-source-maps@^4.0.0:
istanbul-lib-coverage "^3.0.0"
source-map "^0.6.1"
-istanbul-reports@^3.1.3:
+istanbul-reports@^3.0.2, istanbul-reports@^3.1.3:
version "3.1.4"
resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.4.tgz#1b6f068ecbc6c331040aab5741991273e609e40c"
integrity sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==
@@ -5020,6 +9495,19 @@ istanbul-reports@^3.1.3:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
+iterate-iterator@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.2.tgz#551b804c9eaa15b847ea6a7cdc2f5bf1ec150f91"
+ integrity sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==
+
+iterate-value@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57"
+ integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==
+ dependencies:
+ es-get-iterator "^1.0.2"
+ iterate-iterator "^1.0.1"
+
jest-changed-files@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5"
@@ -5160,6 +9648,27 @@ jest-get-type@^27.5.1:
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
+jest-haste-map@^26.6.2:
+ version "26.6.2"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa"
+ integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==
+ dependencies:
+ "@jest/types" "^26.6.2"
+ "@types/graceful-fs" "^4.1.2"
+ "@types/node" "*"
+ anymatch "^3.0.3"
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.2.4"
+ jest-regex-util "^26.0.0"
+ jest-serializer "^26.6.2"
+ jest-util "^26.6.2"
+ jest-worker "^26.6.2"
+ micromatch "^4.0.2"
+ sane "^4.0.3"
+ walker "^1.0.7"
+ optionalDependencies:
+ fsevents "^2.1.2"
+
jest-haste-map@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.5.1.tgz#9fd8bd7e7b4fa502d9c6164c5640512b4e811e7f"
@@ -5236,7 +9745,7 @@ jest-message-util@^27.5.1:
slash "^3.0.0"
stack-utils "^2.0.3"
-jest-mock@^27.5.1:
+jest-mock@^27.0.6, jest-mock@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-27.5.1.tgz#19948336d49ef4d9c52021d34ac7b5f36ff967d6"
integrity sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==
@@ -5249,6 +9758,11 @@ jest-pnp-resolver@^1.2.2:
resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==
+jest-regex-util@^26.0.0:
+ version "26.0.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28"
+ integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==
+
jest-regex-util@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-27.5.1.tgz#4da143f7e9fd1e542d4aa69617b38e4a78365b95"
@@ -5334,6 +9848,14 @@ jest-runtime@^27.5.1:
slash "^3.0.0"
strip-bom "^4.0.0"
+jest-serializer@^26.6.2:
+ version "26.6.2"
+ resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1"
+ integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==
+ dependencies:
+ "@types/node" "*"
+ graceful-fs "^4.2.4"
+
jest-serializer@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-27.5.1.tgz#81438410a30ea66fd57ff730835123dea1fb1f64"
@@ -5370,6 +9892,18 @@ jest-snapshot@^27.5.1:
pretty-format "^27.5.1"
semver "^7.3.2"
+jest-util@^26.6.2:
+ version "26.6.2"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1"
+ integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==
+ dependencies:
+ "@jest/types" "^26.6.2"
+ "@types/node" "*"
+ chalk "^4.0.0"
+ graceful-fs "^4.2.4"
+ is-ci "^2.0.0"
+ micromatch "^4.0.2"
+
jest-util@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.5.1.tgz#3ba9771e8e31a0b85da48fe0b0891fb86c01c2f9"
@@ -5407,7 +9941,16 @@ jest-watcher@^27.5.1:
jest-util "^27.5.1"
string-length "^4.0.1"
-jest-worker@^27.5.1:
+jest-worker@^26.5.0, jest-worker@^26.6.2:
+ version "26.6.2"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
+ integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==
+ dependencies:
+ "@types/node" "*"
+ merge-stream "^2.0.0"
+ supports-color "^7.0.0"
+
+jest-worker@^27.4.5, jest-worker@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0"
integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==
@@ -5425,6 +9968,11 @@ jest@^27.5.1:
import-local "^3.0.2"
jest-cli "^27.5.1"
+js-string-escape@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
+ integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -5488,12 +10036,12 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
-json-parse-better-errors@^1.0.1:
+json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
-json-parse-even-better-errors@^2.3.0:
+json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
@@ -5532,11 +10080,18 @@ json5@^1.0.1:
dependencies:
minimist "^1.2.0"
-json5@^2.2.1:
+json5@^2.1.2, json5@^2.1.3, json5@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug=
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@@ -5564,11 +10119,42 @@ jsonparse@^1.2.0:
array-includes "^3.1.4"
object.assign "^4.1.2"
-kind-of@^6.0.2, kind-of@^6.0.3:
+junk@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
+ integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk=
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
kleur@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
@@ -5579,6 +10165,11 @@ kleur@^4.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
+klona@^2.0.4, klona@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
+ integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
+
known-css-properties@^0.24.0:
version "0.24.0"
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.24.0.tgz#19aefd85003ae5698a5560d2b55135bf5432155c"
@@ -5596,6 +10187,17 @@ language-tags@^1.0.5:
dependencies:
language-subtag-registry "~0.3.2"
+lazy-universal-dotenv@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lazy-universal-dotenv/-/lazy-universal-dotenv-3.0.1.tgz#a6c8938414bca426ab8c9463940da451a911db38"
+ integrity sha512-prXSYk799h3GY3iOWnC6ZigYzMPjxN2svgjJ9shk7oMadSNX3wXy0B6F32PMJv7qtMnrIbUxoEHzbutvxR2LBQ==
+ dependencies:
+ "@babel/runtime" "^7.5.0"
+ app-root-dir "^1.0.2"
+ core-js "^3.0.4"
+ dotenv "^8.0.0"
+ dotenv-expand "^5.1.0"
+
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@@ -5661,6 +10263,17 @@ listr2@^4.0.1:
through "^2.3.8"
wrap-ansi "^7.0.0"
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -5671,6 +10284,39 @@ load-json-file@^4.0.0:
pify "^3.0.0"
strip-bom "^3.0.0"
+loader-runner@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
+ integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
+
+loader-runner@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
+ integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
+
+loader-utils@^1.2.3:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
+ integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+ dependencies:
+ big.js "^5.2.2"
+ emojis-list "^3.0.0"
+ json5 "^1.0.1"
+
+loader-utils@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129"
+ integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==
+ dependencies:
+ big.js "^5.2.2"
+ emojis-list "^3.0.0"
+ json5 "^2.1.2"
+
+loader-utils@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.0.tgz#bcecc51a7898bee7473d4bc6b845b23af8304d4f"
+ integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==
+
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -5721,6 +10367,11 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
+lodash.uniq@4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+ integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -5748,6 +10399,14 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^3.0.0"
+
loud-rejection@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-2.2.0.tgz#4255eb6e9c74045b0edc021fa7397ab655a8517c"
@@ -5756,6 +10415,28 @@ loud-rejection@^2.2.0:
currently-unhandled "^0.4.1"
signal-exit "^3.0.2"
+lower-case@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28"
+ integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
+ dependencies:
+ tslib "^2.0.3"
+
+lowlight@^1.17.0:
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888"
+ integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==
+ dependencies:
+ fault "^1.0.0"
+ highlight.js "~10.7.0"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -5775,7 +10456,15 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.8"
-make-dir@^3.0.0:
+make-dir@^2.0.0, make-dir@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
+ integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
+ dependencies:
+ pify "^4.0.1"
+ semver "^5.6.0"
+
+make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -5794,7 +10483,19 @@ makeerror@1.0.12:
dependencies:
tmpl "1.0.5"
-map-obj@^1.0.0:
+map-age-cleaner@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+ integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+ dependencies:
+ p-defer "^1.0.0"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-obj@^1.0.0, map-obj@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
@@ -5804,6 +10505,23 @@ map-obj@^4.0.0:
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
+map-or-similar@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08"
+ integrity sha1-beJlMXSt+12e3DPGnT6Sobdvrwg=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+markdown-escapes@^1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
+ integrity sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==
+
markdown-extensions@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3"
@@ -5814,6 +10532,29 @@ mathml-tag-names@^2.1.3:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
+md5.js@^1.3.4:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+ integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
+mdast-squeeze-paragraphs@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97"
+ integrity sha512-zxdPn69hkQ1rm4J+2Cs2j6wDEv7O17TfXTJ33tl/+JPIoEmtV9t2ZzBM5LPHE8QlHsmVD8t3vPKCyY3oH+H8MQ==
+ dependencies:
+ unist-util-remove "^2.0.0"
+
+mdast-util-definitions@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2"
+ integrity sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==
+ dependencies:
+ unist-util-visit "^2.0.0"
+
mdast-util-definitions@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz#b6d10ef00a3c4cf191e8d9a5fa58d7f4a366f817"
@@ -5888,6 +10629,20 @@ mdast-util-mdxjs-esm@^1.0.0:
mdast-util-from-markdown "^1.0.0"
mdast-util-to-markdown "^1.0.0"
+mdast-util-to-hast@10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb"
+ integrity sha512-BW3LM9SEMnjf4HXXVApZMt8gLQWVNXc3jryK0nJu/rOXPOnlkUjmdkDlmxMirpbU9ILncGFIwLH/ubnWBbcdgA==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ "@types/unist" "^2.0.0"
+ mdast-util-definitions "^4.0.0"
+ mdurl "^1.0.0"
+ unist-builder "^2.0.0"
+ unist-util-generated "^1.0.0"
+ unist-util-position "^3.0.0"
+ unist-util-visit "^2.0.0"
+
mdast-util-to-hast@^12.1.0:
version "12.1.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.1.1.tgz#89a2bb405eaf3b05eb8bf45157678f35eef5dbca"
@@ -5917,6 +10672,11 @@ mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0:
unist-util-visit "^4.0.0"
zwitch "^2.0.0"
+mdast-util-to-string@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527"
+ integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==
+
mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
@@ -5932,6 +10692,65 @@ mdurl@^1.0.0:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+mem@^8.1.1:
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122"
+ integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==
+ dependencies:
+ map-age-cleaner "^0.1.3"
+ mimic-fn "^3.1.0"
+
+memfs@^3.1.2, memfs@^3.2.2:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305"
+ integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==
+ dependencies:
+ fs-monkey "1.0.3"
+
+memoizerific@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
+ integrity sha1-fIekZGREwy11Q4VwkF8tvRsagFo=
+ dependencies:
+ map-or-similar "^1.5.0"
+
+memory-fs@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+memory-fs@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
+ integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+meow@^3.1.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
meow@^8.0.0:
version "8.1.2"
resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897"
@@ -5967,16 +10786,31 @@ meow@^9.0.0:
type-fest "^0.18.0"
yargs-parser "^20.2.3"
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+ integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
-merge2@^1.3.0, merge2@^1.4.1:
+merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+microevent.ts@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
+ integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
+
micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"
@@ -6261,7 +11095,26 @@ micromark@^3.0.0:
micromark-util-types "^1.0.1"
uvu "^0.5.0"
-micromatch@^4.0.4, micromatch@^4.0.5:
+micromatch@^3.1.10, micromatch@^3.1.4:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
@@ -6269,34 +11122,74 @@ micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.2"
picomatch "^2.3.1"
-mime-db@1.52.0:
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
-mime-types@^2.1.12:
+mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.30, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
+mime@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+mime@^2.4.4:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
+ integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
+
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+mimic-fn@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74"
+ integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==
+
mimic-response@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
+min-document@^2.19.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
+ integrity sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=
+ dependencies:
+ dom-walk "^0.1.0"
+
min-indent@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-minimatch@^3.0.4, minimatch@^3.1.2:
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+ integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -6312,16 +11205,88 @@ minimist-options@4.1.0:
is-plain-obj "^1.1.0"
kind-of "^6.0.3"
-minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+minipass-collect@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
+ integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==
+ dependencies:
+ minipass "^3.0.0"
+
+minipass-flush@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
+ integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==
+ dependencies:
+ minipass "^3.0.0"
+
+minipass-pipeline@^1.2.2:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
+ integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
+ dependencies:
+ minipass "^3.0.0"
+
+minipass@^3.0.0, minipass@^3.1.1:
+ version "3.1.6"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
+ integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==
+ dependencies:
+ yallist "^4.0.0"
+
+minizlib@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
+ integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
+ dependencies:
+ minipass "^3.0.0"
+ yallist "^4.0.0"
+
+mississippi@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+ integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^3.0.0"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
+mixin-deep@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+ integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
+mkdirp@^0.5.1, mkdirp@^0.5.3:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+ integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+ dependencies:
+ minimist "^1.2.6"
+
+mkdirp@^1.0.3, mkdirp@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
+ integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+
modern-normalize@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7"
@@ -6332,6 +11297,18 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
+move-concurrently@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+ integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+ dependencies:
+ aproba "^1.1.1"
+ copy-concurrently "^1.0.0"
+ fs-write-stream-atomic "^1.0.8"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.3"
+
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
@@ -6347,21 +11324,48 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+ms@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-ms@^2.1.1:
+ms@2.1.3, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+nan@^2.12.1:
+ version "2.15.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
+ integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
+
nanoid@^3.1.30, nanoid@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.2.tgz#c89622fafb4381cd221421c69ec58547a1eec557"
integrity sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
@@ -6372,11 +11376,21 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
-neo-async@^2.6.0:
+negotiator@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+ integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
+neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5"
+ integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==
+
next-router-mock@^0.6.7:
version "0.6.7"
resolved "https://registry.yarnpkg.com/next-router-mock/-/next-router-mock-0.6.7.tgz#8883ed81f245074462e72199fe94002cd85db5e2"
@@ -6418,6 +11432,19 @@ next@^12.1.5:
"@next/swc-win32-ia32-msvc" "12.1.5"
"@next/swc-win32-x64-msvc" "12.1.5"
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+no-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d"
+ integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
+ dependencies:
+ lower-case "^2.0.2"
+ tslib "^2.0.3"
+
node-abi@^3.3.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.8.0.tgz#679957dc8e7aa47b0a02589dbfde4f77b29ccb32"
@@ -6430,7 +11457,14 @@ node-addon-api@^4.3.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
-node-fetch@2.6.7:
+node-dir@^0.1.10:
+ version "0.1.17"
+ resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"
+ integrity sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=
+ dependencies:
+ minimatch "^3.0.2"
+
+node-fetch@^2.6.1, node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@@ -6442,12 +11476,41 @@ node-int64@^0.4.0:
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+node-libs-browser@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
+ integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.2.0"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^3.0.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
+ path-browserify "0.0.1"
+ process "^0.11.10"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.3.3"
+ stream-browserify "^2.0.1"
+ stream-http "^2.7.2"
+ string_decoder "^1.0.0"
+ timers-browserify "^2.0.4"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.11.0"
+ vm-browserify "^1.0.1"
+
node-releases@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.3.tgz#225ee7488e4a5e636da8da52854844f9d716ca96"
integrity sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==
-normalize-package-data@^2.3.2, normalize-package-data@^2.5.0:
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
@@ -6467,16 +11530,35 @@ normalize-package-data@^3.0.0:
semver "^7.3.4"
validate-npm-package-license "^3.0.1"
+normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+ integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
normalize-selector@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ dependencies:
+ path-key "^2.0.0"
+
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -6494,6 +11576,16 @@ npmlog@^4.0.1:
gauge "~2.7.3"
set-blocking "~2.0.0"
+npmlog@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
+ integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
+ dependencies:
+ are-we-there-yet "^2.0.0"
+ console-control-strings "^1.1.0"
+ gauge "^3.0.0"
+ set-blocking "^2.0.0"
+
nth-check@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2"
@@ -6506,6 +11598,11 @@ null-check@^1.0.0:
resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+ integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
@@ -6516,21 +11613,37 @@ nwsapi@^2.2.0:
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
-object-assign@^4.1.0, object-assign@^4.1.1:
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
object-inspect@^1.12.0, object-inspect@^1.9.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
-object-keys@^1.1.1:
+object-keys@^1.0.12, object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
object.assign@^4.1.0, object.assign@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
@@ -6541,7 +11654,7 @@ object.assign@^4.1.0, object.assign@^4.1.2:
has-symbols "^1.0.1"
object-keys "^1.1.1"
-object.entries@^1.1.5:
+object.entries@^1.1.0, object.entries@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861"
integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==
@@ -6550,7 +11663,7 @@ object.entries@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
-object.fromentries@^2.0.5:
+"object.fromentries@^2.0.0 || ^1.0.0", object.fromentries@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251"
integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==
@@ -6559,6 +11672,15 @@ object.fromentries@^2.0.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
+object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.2:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e"
+ integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.1"
+
object.hasown@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5"
@@ -6567,7 +11689,14 @@ object.hasown@^1.1.0:
define-properties "^1.1.3"
es-abstract "^1.19.1"
-object.values@^1.1.5:
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.1.0, object.values@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
@@ -6576,6 +11705,23 @@ object.values@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
+objectorarray@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5"
+ integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+ integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -6590,6 +11736,23 @@ onetime@^5.1.0, onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
+open@^7.0.3:
+ version "7.4.2"
+ resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
+ integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
+ dependencies:
+ is-docker "^2.0.0"
+ is-wsl "^2.1.1"
+
+open@^8.4.0:
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
+ integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==
+ dependencies:
+ define-lazy-prop "^2.0.0"
+ is-docker "^2.1.1"
+ is-wsl "^2.2.0"
+
opener@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
@@ -6619,6 +11782,47 @@ optionator@^0.9.1:
type-check "^0.4.0"
word-wrap "^1.2.3"
+os-browserify@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+ integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+p-all@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-all/-/p-all-2.1.0.tgz#91419be56b7dee8fe4c5db875d55e0da084244a0"
+ integrity sha512-HbZxz5FONzz/z2gJfk6bFca0BCiSRF8jU3yCsWOen/vR6lZjfPOu/e7L3uFzTW1i0H8TlC3vqQstEJPQL4/uLA==
+ dependencies:
+ p-map "^2.0.0"
+
+p-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+ integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
+p-event@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
+ integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
+ dependencies:
+ p-timeout "^3.1.0"
+
+p-filter@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c"
+ integrity sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==
+ dependencies:
+ p-map "^2.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -6668,6 +11872,18 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
+p-map@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+ integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+p-map@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d"
+ integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==
+ dependencies:
+ aggregate-error "^3.0.0"
+
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
@@ -6675,6 +11891,13 @@ p-map@^4.0.0:
dependencies:
aggregate-error "^3.0.0"
+p-timeout@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+ integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+ dependencies:
+ p-finally "^1.0.0"
+
p-try@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
@@ -6685,6 +11908,28 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+pako@~1.0.5:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+ integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+parallel-transform@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
+ integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==
+ dependencies:
+ cyclist "^1.0.1"
+ inherits "^2.0.3"
+ readable-stream "^2.1.5"
+
+param-case@^3.0.3, param-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
+ integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==
+ dependencies:
+ dot-case "^3.0.4"
+ tslib "^2.0.3"
+
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -6692,6 +11937,29 @@ parent-module@^1.0.0:
dependencies:
callsites "^3.0.0"
+parse-asn1@^5.0.0, parse-asn1@^5.1.5:
+ version "5.1.6"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
+ integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
+ dependencies:
+ asn1.js "^5.2.0"
+ browserify-aes "^1.0.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+ safe-buffer "^5.1.1"
+
+parse-entities@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
+ integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
+ dependencies:
+ character-entities "^1.0.0"
+ character-entities-legacy "^1.0.0"
+ character-reference-invalid "^1.0.0"
+ is-alphanumerical "^1.0.0"
+ is-decimal "^1.0.0"
+ is-hexadecimal "^1.0.0"
+
parse-entities@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.0.tgz#f67c856d4e3fe19b1a445c3fabe78dcdc1053eeb"
@@ -6706,6 +11974,13 @@ parse-entities@^4.0.0:
is-decimal "^2.0.0"
is-hexadecimal "^2.0.0"
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+ dependencies:
+ error-ex "^1.2.0"
+
parse-json@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -6724,11 +11999,51 @@ parse-json@^5.0.0, parse-json@^5.2.0:
json-parse-even-better-errors "^2.3.0"
lines-and-columns "^1.1.6"
-parse5@6.0.1:
+parse5@6.0.1, parse5@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+parseurl@~1.3.2, parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+pascal-case@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb"
+ integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
+ dependencies:
+ no-case "^3.0.4"
+ tslib "^2.0.3"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-browserify@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
+ integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==
+
+path-browserify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
+ integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+ integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
+ dependencies:
+ pinkie-promise "^2.0.0"
+
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
@@ -6744,6 +12059,11 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@@ -6754,6 +12074,20 @@ path-parse@^1.0.6, path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+ integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
path-type@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
@@ -6766,6 +12100,17 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+pbkdf2@^3.0.3:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
+ integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
periscopic@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.0.4.tgz#b3fbed0d1bc844976b977173ca2cd4a0ef4fa8d1"
@@ -6774,12 +12119,17 @@ periscopic@^3.0.0:
estree-walker "^3.0.0"
is-reference "^3.0.0"
+picocolors@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
+ integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==
+
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.0, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@@ -6789,7 +12139,7 @@ pidtree@^0.5.0:
resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.5.0.tgz#ad5fbc1de78b8a5f99d6fbdd4f6e4eee21d1aca1"
integrity sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==
-pify@^2.3.0:
+pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
@@ -6799,28 +12149,173 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
-pirates@^4.0.4:
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+ integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+ integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+
+pirates@^4.0.1, pirates@^4.0.4, pirates@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b"
integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==
-pkg-dir@^4.2.0:
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
+pkg-dir@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760"
+ integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==
+ dependencies:
+ find-up "^5.0.0"
+
platform@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
+pnp-webpack-plugin@1.6.4:
+ version "1.6.4"
+ resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149"
+ integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg==
+ dependencies:
+ ts-pnp "^1.1.6"
+
+polished@^4.0.5:
+ version "4.1.4"
+ resolved "https://registry.yarnpkg.com/polished/-/polished-4.1.4.tgz#640293ba834109614961a700fdacbb6599fb12d0"
+ integrity sha512-Nq5Mbza+Auo7N3sQb1QMFaQiDO+4UexWuSGR7Cjb4Sw11SZIJcrrFtiZ+L0jT9MBsUsxDboHVASbCLbE1rnECg==
+ dependencies:
+ "@babel/runtime" "^7.16.7"
+
+polished@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"
+ integrity sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==
+ dependencies:
+ "@babel/runtime" "^7.17.8"
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+postcss-flexbugs-fixes@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.1.tgz#9218a65249f30897deab1033aced8578562a6690"
+ integrity sha512-9SiofaZ9CWpQWxOwRh1b/r85KD5y7GgvsNt1056k6OYLvWUun0czCvogfJgylC22uJTwW1KzY3Gz65NZRlvoiQ==
+ dependencies:
+ postcss "^7.0.26"
+
+postcss-loader@^4.2.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-4.3.0.tgz#2c4de9657cd4f07af5ab42bd60a673004da1b8cc"
+ integrity sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==
+ dependencies:
+ cosmiconfig "^7.0.0"
+ klona "^2.0.4"
+ loader-utils "^2.0.0"
+ schema-utils "^3.0.0"
+ semver "^7.3.4"
+
+postcss-loader@^6.2.1:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef"
+ integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==
+ dependencies:
+ cosmiconfig "^7.0.0"
+ klona "^2.0.5"
+ semver "^7.3.5"
+
postcss-media-query-parser@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=
+postcss-modules-extract-imports@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
+ integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
+ dependencies:
+ postcss "^7.0.5"
+
+postcss-modules-extract-imports@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
+ integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
+
+postcss-modules-local-by-default@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0"
+ integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==
+ dependencies:
+ icss-utils "^4.1.1"
+ postcss "^7.0.32"
+ postcss-selector-parser "^6.0.2"
+ postcss-value-parser "^4.1.0"
+
+postcss-modules-local-by-default@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
+ integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+ dependencies:
+ icss-utils "^5.0.0"
+ postcss-selector-parser "^6.0.2"
+ postcss-value-parser "^4.1.0"
+
+postcss-modules-scope@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
+ integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
+ dependencies:
+ postcss "^7.0.6"
+ postcss-selector-parser "^6.0.0"
+
+postcss-modules-scope@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
+ integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
+ dependencies:
+ postcss-selector-parser "^6.0.4"
+
+postcss-modules-values@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10"
+ integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==
+ dependencies:
+ icss-utils "^4.0.0"
+ postcss "^7.0.6"
+
+postcss-modules-values@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
+ integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
+ dependencies:
+ icss-utils "^5.0.0"
+
postcss-resolve-nested-selector@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e"
@@ -6836,6 +12331,14 @@ postcss-scss@^4.0.2:
resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.3.tgz#36c23c19a804274e722e83a54d20b838ab4767ac"
integrity sha512-j4KxzWovfdHsyxwl1BxkUal/O4uirvHgdzMKS1aWJBAV0qh2qj5qAZqpeBfVUYGWv+4iK9Az7SPyZ4fyNju1uA==
+postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
+ version "6.0.9"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f"
+ integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ==
+ dependencies:
+ cssesc "^3.0.0"
+ util-deprecate "^1.0.2"
+
postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.6:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
@@ -6858,7 +12361,15 @@ postcss@8.4.5:
picocolors "^1.0.0"
source-map-js "^1.0.1"
-postcss@^8.1.10, postcss@^8.4.12:
+postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.36, postcss@^7.0.5, postcss@^7.0.6:
+ version "7.0.39"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
+ integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
+ dependencies:
+ picocolors "^0.2.1"
+ source-map "^0.6.1"
+
+postcss@^8.1.10, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.4.12:
version "8.4.12"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905"
integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==
@@ -6896,11 +12407,32 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+"prettier@>=2.2.1 <=2.3.0":
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
+ integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
+
prettier@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
+pretty-error@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"
+ integrity sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==
+ dependencies:
+ lodash "^4.17.20"
+ renderkid "^2.0.4"
+
+pretty-error@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6"
+ integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==
+ dependencies:
+ lodash "^4.17.20"
+ renderkid "^3.0.0"
+
pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1:
version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
@@ -6910,7 +12442,12 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1:
ansi-styles "^5.0.0"
react-is "^17.0.1"
-prismjs@^1.27.0:
+pretty-hrtime@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
+ integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
+
+prismjs@^1.27.0, prismjs@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
@@ -6920,7 +12457,38 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-prompts@^2.0.1:
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+promise-inflight@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+ integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+promise.allsettled@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.5.tgz#2443f3d4b2aa8dfa560f6ac2aa6c4ea999d75f53"
+ integrity sha512-tVDqeZPoBC0SlzJHzWGZ2NKAguVq2oiYj7gbggbiTvH2itHohijTp7njOUA0aQ/nl+0lr/r6egmhoYu63UZ/pQ==
+ dependencies:
+ array.prototype.map "^1.0.4"
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.1"
+ get-intrinsic "^1.1.1"
+ iterate-value "^1.0.2"
+
+promise.prototype.finally@^3.1.0:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/promise.prototype.finally/-/promise.prototype.finally-3.1.3.tgz#d3186e58fcf4df1682a150f934ccc27b7893389c"
+ integrity sha512-EXRF3fC9/0gz4qkt/f5EP5iW4kj9oFpBICNpCNOb/52+8nlHIX07FPLbi/q4qYBQ1xZqivMzTpNQSnArVASolQ==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.1"
+
+prompts@^2.0.1, prompts@^2.4.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
@@ -6928,7 +12496,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@^15.8.1:
+prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -6937,16 +12505,56 @@ prop-types@^15.8.1:
object-assign "^4.1.1"
react-is "^16.13.1"
+property-information@^5.0.0, property-information@^5.3.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.6.0.tgz#61675545fb23002f245c6540ec46077d4da3ed69"
+ integrity sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==
+ dependencies:
+ xtend "^4.0.0"
+
property-information@^6.0.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.1.1.tgz#5ca85510a3019726cb9afed4197b7b8ac5926a22"
integrity sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==
+proxy-addr@~2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+ integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
+ dependencies:
+ forwarded "0.2.0"
+ ipaddr.js "1.9.1"
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+ integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
psl@^1.1.33:
version "1.8.0"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+public-encrypt@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
+ integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+ safe-buffer "^5.1.2"
+
+pump@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+ integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
@@ -6955,6 +12563,25 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
+pumpify@^1.3.3:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
+ integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
+ dependencies:
+ duplexify "^3.6.0"
+ inherits "^2.0.3"
+ pump "^2.0.0"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+ integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@@ -6965,16 +12592,93 @@ q@^1.5.1:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+qs@6.9.7:
+ version "6.9.7"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
+ integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
+
+qs@^6.10.0:
+ version "6.10.3"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
+ integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==
+ dependencies:
+ side-channel "^1.0.4"
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+ integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+ integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+querystring@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd"
+ integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+queue@6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65"
+ integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==
+ dependencies:
+ inherits "~2.0.3"
+
quick-lru@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
+ramda@^0.21.0:
+ version "0.21.0"
+ resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35"
+ integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU=
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+ integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+ integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
+ dependencies:
+ randombytes "^2.0.5"
+ safe-buffer "^5.1.0"
+
+range-parser@^1.2.1, range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.3.tgz#8f80305d11c2a0a545c2d9d89d7a0286fcead43c"
+ integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==
+ dependencies:
+ bytes "3.1.2"
+ http-errors "1.8.1"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+raw-loader@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6"
+ integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==
+ dependencies:
+ loader-utils "^2.0.0"
+ schema-utils "^3.0.0"
+
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -6985,6 +12689,27 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-docgen-typescript@^2.1.1:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c"
+ integrity sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==
+
+react-docgen@^5.0.0:
+ version "5.4.0"
+ resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-5.4.0.tgz#2cd7236720ec2769252ef0421f23250b39a153a1"
+ integrity sha512-JBjVQ9cahmNlfjMGxWUxJg919xBBKAoy3hgDgKERbR+BcF4ANpDuzWAScC7j27hZfd8sJNmMPOLWo9+vB/XJEQ==
+ dependencies:
+ "@babel/core" "^7.7.5"
+ "@babel/generator" "^7.12.11"
+ "@babel/runtime" "^7.7.6"
+ ast-types "^0.14.2"
+ commander "^2.19.0"
+ doctrine "^3.0.0"
+ estree-to-babel "^3.1.0"
+ neo-async "^2.6.1"
+ node-dir "^0.1.10"
+ strip-indent "^3.0.0"
+
react-dom@^18.0.0:
version "18.0.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023"
@@ -6993,31 +12718,85 @@ react-dom@^18.0.0:
loose-envify "^1.1.0"
scheduler "^0.21.0"
-react-intl@^5.24.8:
- version "5.24.8"
- resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.24.8.tgz#8387205a8e125ce057fc260108b02ad00b22e9b6"
- integrity sha512-uFBA7Fvh3XsHVn6b+jgVTk8hMBpQFvkterWwq4KHrjn8nMmLJf6lGqPawAcmhXes0q29JruCQyKX0vj+G7iokA==
+react-element-to-jsx-string@^14.3.4:
+ version "14.3.4"
+ resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz#709125bc72f06800b68f9f4db485f2c7d31218a8"
+ integrity sha512-t4ZwvV6vwNxzujDQ+37bspnLwA4JlgUPWhLjBJWsNIDceAf6ZKUTCjdm08cN6WeZ5pTMKiCJkmAYnpmR4Bm+dg==
+ dependencies:
+ "@base2/pretty-print-object" "1.0.1"
+ is-plain-object "5.0.0"
+ react-is "17.0.2"
+
+react-inspector@^5.1.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.1.tgz#58476c78fde05d5055646ed8ec02030af42953c8"
+ integrity sha512-GURDaYzoLbW8pMGXwYPDBIv6nqei4kK7LPRZ9q9HCZF54wqXz/dnylBp/kfE9XmekBhHvLDdcYeyIwSrvtOiWg==
+ dependencies:
+ "@babel/runtime" "^7.0.0"
+ is-dom "^1.0.0"
+ prop-types "^15.0.0"
+
+react-intl@^5.25.0:
+ version "5.25.0"
+ resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-5.25.0.tgz#8827e5fdb839055028b53005a221d1995c386591"
+ integrity sha512-jIgmCy9s2IVFdQHEe2LzW023wKDQwgKXPdxg6WwuUJxR9BHPBPGLj01rxc3gLZ3aKDuL91SsFPAlx+qEy7+k0w==
dependencies:
"@formatjs/ecma402-abstract" "1.11.4"
"@formatjs/icu-messageformat-parser" "2.0.19"
- "@formatjs/intl" "2.1.1"
+ "@formatjs/intl" "2.2.0"
"@formatjs/intl-displaynames" "5.4.3"
"@formatjs/intl-listformat" "6.5.3"
"@types/hoist-non-react-statics" "^3.3.1"
- "@types/react" "16 || 17"
+ "@types/react" "16 || 17 || 18"
hoist-non-react-statics "^3.3.2"
intl-messageformat "9.12.0"
tslib "^2.1.0"
+react-is@17.0.2, react-is@^17.0.1:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-react-is@^17.0.1:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
- integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+react-merge-refs@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
+ integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
+
+react-refresh@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
+ integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==
+
+react-router-dom@^6.0.0:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.2.2.tgz#f1a2c88365593c76b9612ae80154a13fcb72e442"
+ integrity sha512-AtYEsAST7bDD4dLSQHDnk/qxWLJdad5t1HFa1qJyUrCeGgEuCSw0VB/27ARbF9Fi/W5598ujvJOm3ujUCVzuYQ==
+ dependencies:
+ history "^5.2.0"
+ react-router "6.2.2"
+
+react-router@6.2.2, react-router@^6.0.0:
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.2.2.tgz#495e683a0c04461eeb3d705fe445d6cf42f0c249"
+ integrity sha512-/MbxyLzd7Q7amp4gDOGaYvXwhEojkJD5BtExkuKmj39VEE0m3l/zipf6h2WIB2jyAO0lI6NGETh4RDcktRm4AQ==
+ dependencies:
+ history "^5.2.0"
+
+react-syntax-highlighter@^15.4.5:
+ version "15.5.0"
+ resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
+ integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==
+ dependencies:
+ "@babel/runtime" "^7.3.1"
+ highlight.js "^10.4.1"
+ lowlight "^1.17.0"
+ prismjs "^1.27.0"
+ refractor "^3.6.0"
react@^18.0.0:
version "18.0.0"
@@ -7026,6 +12805,14 @@ react@^18.0.0:
dependencies:
loose-envify "^1.1.0"
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
read-pkg-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
@@ -7043,6 +12830,15 @@ read-pkg-up@^7.0.1:
read-pkg "^5.2.0"
type-fest "^0.8.1"
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
read-pkg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
@@ -7062,16 +12858,7 @@ read-pkg@^5.2.0:
parse-json "^5.0.0"
type-fest "^0.6.0"
-readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
- integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
- dependencies:
- inherits "^2.0.3"
- string_decoder "^1.1.1"
- util-deprecate "^1.0.1"
-
-readable-stream@^2.0.6, readable-stream@~2.3.6:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -7084,6 +12871,24 @@ readable-stream@^2.0.6, readable-stream@~2.3.6:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
+readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+ integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+ integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+ micromatch "^3.1.10"
+ readable-stream "^2.0.2"
+
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -7091,6 +12896,14 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -7099,6 +12912,15 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
+refractor@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"
+ integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==
+ dependencies:
+ hastscript "^6.0.0"
+ parse-entities "^2.0.0"
+ prismjs "~1.27.0"
+
regenerate-unicode-properties@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56"
@@ -7111,7 +12933,7 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
-regenerator-runtime@^0.13.4:
+regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
@@ -7123,6 +12945,19 @@ regenerator-transform@^0.15.0:
dependencies:
"@babel/runtime" "^7.8.4"
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+regex-parser@^2.2.11:
+ version "2.2.11"
+ resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58"
+ integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==
+
regexp.prototype.flags@^1.4.1:
version "1.4.3"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac"
@@ -7161,6 +12996,41 @@ regjsparser@^0.8.2:
dependencies:
jsesc "~0.5.0"
+relateurl@^0.2.7:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
+ integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
+
+remark-external-links@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/remark-external-links/-/remark-external-links-8.0.0.tgz#308de69482958b5d1cd3692bc9b725ce0240f345"
+ integrity sha512-5vPSX0kHoSsqtdftSHhIYofVINC8qmp0nctkeU9YoJwV3YfiBRiI6cbFRJ0oI/1F9xS+bopXG0m2KS8VFscuKA==
+ dependencies:
+ extend "^3.0.0"
+ is-absolute-url "^3.0.0"
+ mdast-util-definitions "^4.0.0"
+ space-separated-tokens "^1.0.0"
+ unist-util-visit "^2.0.0"
+
+remark-footnotes@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/remark-footnotes/-/remark-footnotes-2.0.0.tgz#9001c4c2ffebba55695d2dd80ffb8b82f7e6303f"
+ integrity sha512-3Clt8ZMH75Ayjp9q4CorNeyjwIxHFcTkaektplKGl2A1jNGEUey8cKL0ZC5vJwfcD5GFGsNLImLG/NGzWIzoMQ==
+
+remark-mdx@1.6.22:
+ version "1.6.22"
+ resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-1.6.22.tgz#06a8dab07dcfdd57f3373af7f86bd0e992108bbd"
+ integrity sha512-phMHBJgeV76uyFkH4rvzCftLfKCr2RZuF+/gmVcaKrpsihyzmhXjA0BEMDaPTXG5y8qZOKPVo83NAOX01LPnOQ==
+ dependencies:
+ "@babel/core" "7.12.9"
+ "@babel/helper-plugin-utils" "7.10.4"
+ "@babel/plugin-proposal-object-rest-spread" "7.12.1"
+ "@babel/plugin-syntax-jsx" "7.12.1"
+ "@mdx-js/util" "1.6.22"
+ is-alphabetical "1.0.4"
+ remark-parse "8.0.3"
+ unified "9.2.0"
+
remark-mdx@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-2.1.1.tgz#14021be9ecbc9ad0310f4240980221328aa7ed55"
@@ -7169,6 +13039,28 @@ remark-mdx@^2.0.0:
mdast-util-mdx "^2.0.0"
micromark-extension-mdxjs "^1.0.0"
+remark-parse@8.0.3:
+ version "8.0.3"
+ resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1"
+ integrity sha512-E1K9+QLGgggHxCQtLt++uXltxEprmWzNfg+MxpfHsZlrddKzZ/hZyWHDbK3/Ap8HJQqYJRXP+jHczdL6q6i85Q==
+ dependencies:
+ ccount "^1.0.0"
+ collapse-white-space "^1.0.2"
+ is-alphabetical "^1.0.0"
+ is-decimal "^1.0.0"
+ is-whitespace-character "^1.0.0"
+ is-word-character "^1.0.0"
+ markdown-escapes "^1.0.0"
+ parse-entities "^2.0.0"
+ repeat-string "^1.5.4"
+ state-toggle "^1.0.0"
+ trim "0.0.1"
+ trim-trailing-lines "^1.0.0"
+ unherit "^1.0.4"
+ unist-util-remove-position "^2.0.0"
+ vfile-location "^3.0.0"
+ xtend "^4.0.1"
+
remark-parse@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
@@ -7188,6 +13080,66 @@ remark-rehype@^10.0.0:
mdast-util-to-hast "^12.1.0"
unified "^10.0.0"
+remark-slug@^6.0.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-6.1.0.tgz#0503268d5f0c4ecb1f33315c00465ccdd97923ce"
+ integrity sha512-oGCxDF9deA8phWvxFuyr3oSJsdyUAxMFbA0mZ7Y1Sas+emILtO+e5WutF9564gDsEN4IXaQXm5pFo6MLH+YmwQ==
+ dependencies:
+ github-slugger "^1.0.0"
+ mdast-util-to-string "^1.0.0"
+ unist-util-visit "^2.0.0"
+
+remark-squeeze-paragraphs@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-4.0.0.tgz#76eb0e085295131c84748c8e43810159c5653ead"
+ integrity sha512-8qRqmL9F4nuLPIgl92XUuxI3pFxize+F1H0e/W3llTk0UsjJaj01+RrirkMw7P21RKe4X6goQhYRSvNWX+70Rw==
+ dependencies:
+ mdast-squeeze-paragraphs "^4.0.0"
+
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+renderkid@^2.0.4:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609"
+ integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==
+ dependencies:
+ css-select "^4.1.3"
+ dom-converter "^0.2.0"
+ htmlparser2 "^6.1.0"
+ lodash "^4.17.21"
+ strip-ansi "^3.0.1"
+
+renderkid@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a"
+ integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==
+ dependencies:
+ css-select "^4.1.3"
+ dom-converter "^0.2.0"
+ htmlparser2 "^6.1.0"
+ lodash "^4.17.21"
+ strip-ansi "^6.0.1"
+
+repeat-element@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9"
+ integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==
+
+repeat-string@^1.5.4, repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
+ dependencies:
+ is-finite "^1.0.0"
+
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -7198,6 +13150,11 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+requireindex@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
+ integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==
+
resolve-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
@@ -7222,12 +13179,28 @@ resolve-global@1.0.0, resolve-global@^1.0.0:
dependencies:
global-dirs "^0.1.1"
+resolve-url-loader@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz#ee3142fb1f1e0d9db9524d539cfa166e9314f795"
+ integrity sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==
+ dependencies:
+ adjust-sourcemap-loader "^4.0.0"
+ convert-source-map "^1.7.0"
+ loader-utils "^2.0.0"
+ postcss "^8.2.14"
+ source-map "0.6.1"
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
resolve.exports@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-1.1.0.tgz#5ce842b94b05146c0e03076985d1d0e7e48c90c9"
integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==
-resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.20.0:
+resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2:
version "1.22.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
@@ -7252,6 +13225,11 @@ restore-cursor@^3.1.0:
onetime "^5.1.0"
signal-exit "^3.0.2"
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@@ -7262,6 +13240,13 @@ rfdc@^1.3.0:
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
+rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+ integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+ dependencies:
+ glob "^7.1.3"
+
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -7269,6 +13254,19 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+ integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+rsvp@^4.8.4:
+ version "4.8.5"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
+ integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
+
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -7276,6 +13274,13 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
+run-queue@^1.0.0, run-queue@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+ integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
+ dependencies:
+ aproba "^1.1.1"
+
rxjs@^7.5.5:
version "7.5.5"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f"
@@ -7290,21 +13295,56 @@ sade@^1.7.3:
dependencies:
mri "^1.1.0"
-safe-buffer@^5.0.1, safe-buffer@~5.2.0:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
- integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+safe-buffer@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+ integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==
-safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-"safer-buffer@>= 2.1.2 < 3":
+safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+sane@^4.0.3:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
+ integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==
+ dependencies:
+ "@cnakazawa/watch" "^1.0.3"
+ anymatch "^2.0.0"
+ capture-exit "^2.0.0"
+ exec-sh "^0.3.2"
+ execa "^1.0.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+
+sass-loader@^12.4.0:
+ version "12.6.0"
+ resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb"
+ integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==
+ dependencies:
+ klona "^2.0.4"
+ neo-async "^2.6.2"
+
sass@^1.50.0:
version "1.50.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.50.0.tgz#3e407e2ebc53b12f1e35ce45efb226ea6063c7c8"
@@ -7338,7 +13378,43 @@ schema-dts@^1.1.0:
resolved "https://registry.yarnpkg.com/schema-dts/-/schema-dts-1.1.0.tgz#33227971076ef1daa33e56a3127a8aae030e81c9"
integrity sha512-vdmbs/5ycj4zyKpZIDqTcy+IZi4s7c38RVAYuDmRi7zgxUT8wRWPMLzg0jr7FjdVunYu9yZ00F3+XcZTTFcTOQ==
-"semver@2 || 3 || 4 || 5":
+schema-utils@2.7.0:
+ version "2.7.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7"
+ integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
+ dependencies:
+ "@types/json-schema" "^7.0.4"
+ ajv "^6.12.2"
+ ajv-keywords "^3.4.1"
+
+schema-utils@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
+ integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
+ dependencies:
+ ajv "^6.1.0"
+ ajv-errors "^1.0.0"
+ ajv-keywords "^3.1.0"
+
+schema-utils@^2.6.5, schema-utils@^2.7.0:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7"
+ integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==
+ dependencies:
+ "@types/json-schema" "^7.0.5"
+ ajv "^6.12.4"
+ ajv-keywords "^3.5.2"
+
+schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
+ integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
+ dependencies:
+ "@types/json-schema" "^7.0.8"
+ ajv "^6.12.5"
+ ajv-keywords "^3.5.2"
+
+"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -7367,11 +13443,107 @@ semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
dependencies:
lru-cache "^6.0.0"
-set-blocking@~2.0.0:
+send@0.17.2:
+ version "0.17.2"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820"
+ integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.2"
+ destroy "~1.0.4"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "1.8.1"
+ mime "1.6.0"
+ ms "2.1.3"
+ on-finished "~2.3.0"
+ range-parser "~1.2.1"
+ statuses "~1.5.0"
+
+serialize-javascript@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
+ integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
+ dependencies:
+ randombytes "^2.1.0"
+
+serialize-javascript@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
+ integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+ dependencies:
+ randombytes "^2.1.0"
+
+serialize-javascript@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+ integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+ dependencies:
+ randombytes "^2.1.0"
+
+serve-favicon@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/serve-favicon/-/serve-favicon-2.5.0.tgz#935d240cdfe0f5805307fdfe967d88942a2cbcf0"
+ integrity sha1-k10kDN/g9YBTB/3+ln2IlCosvPA=
+ dependencies:
+ etag "~1.8.1"
+ fresh "0.5.2"
+ ms "2.1.1"
+ parseurl "~1.3.2"
+ safe-buffer "5.1.1"
+
+serve-static@1.14.2:
+ version "1.14.2"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa"
+ integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==
+ dependencies:
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "0.17.2"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+set-value@^2.0.0, set-value@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
+ integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+ integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.11"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+ integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shallow-clone@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+ integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+ dependencies:
+ kind-of "^6.0.2"
+
sharp@^0.30.3:
version "0.30.3"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.3.tgz#315a1817423a4d1cde5119a21c99c234a7a6fb37"
@@ -7386,6 +13558,13 @@ sharp@^0.30.3:
tar-fs "^2.1.1"
tunnel-agent "^0.6.0"
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ dependencies:
+ shebang-regex "^1.0.0"
+
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -7393,6 +13572,11 @@ shebang-command@^2.0.0:
dependencies:
shebang-regex "^3.0.0"
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
@@ -7447,6 +13631,11 @@ sisteransi@^1.0.5:
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+slash@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
+ integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
+
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@@ -7478,11 +13667,57 @@ slice-ansi@^5.0.0:
ansi-styles "^6.0.0"
is-fullwidth-code-point "^4.0.0"
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+source-list-map@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
+ integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
+source-map-resolve@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
+ integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==
+ dependencies:
+ atob "^2.1.2"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
source-map-resolve@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2"
@@ -7491,7 +13726,7 @@ source-map-resolve@^0.6.0:
atob "^2.1.2"
decode-uri-component "^0.2.0"
-source-map-support@^0.5.6:
+source-map-support@^0.5.16, source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.20:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
@@ -7499,17 +13734,22 @@ source-map-support@^0.5.6:
buffer-from "^1.0.0"
source-map "^0.6.0"
-source-map@^0.5.0:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
- integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+source-map-url@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
+ integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==
-source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-source-map@^0.7.0, source-map@^0.7.3:
+source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.7.0, source-map@^0.7.3, source-map@~0.7.2:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
@@ -7519,6 +13759,11 @@ sourcemap-codec@^1.4.8:
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
+space-separated-tokens@^1.0.0:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
+ integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
+
space-separated-tokens@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz#43193cec4fb858a2ce934b7f98b7f2c18107098b"
@@ -7555,6 +13800,13 @@ specificity@^0.4.1:
resolved "https://registry.yarnpkg.com/specificity/-/specificity-0.4.1.tgz#aab5e645012db08ba182e151165738d00887b019"
integrity sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
split2@^3.0.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f"
@@ -7574,6 +13826,20 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+ssri@^6.0.1:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
+ integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==
+ dependencies:
+ figgy-pudding "^3.5.1"
+
+ssri@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
+ integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
+ dependencies:
+ minipass "^3.1.1"
+
stable@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
@@ -7586,6 +13852,11 @@ stack-utils@^2.0.3:
dependencies:
escape-string-regexp "^2.0.0"
+stackframe@^1.1.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.1.tgz#1033a3473ee67f08e2f2fc8eba6aef4f845124e1"
+ integrity sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==
+
standard-version@^9.3.2:
version "9.3.2"
resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.3.2.tgz#28db8c1be66fd2d736f28f7c5de7619e64cd6dab"
@@ -7607,6 +13878,82 @@ standard-version@^9.3.2:
stringify-package "^1.0.1"
yargs "^16.0.0"
+state-toggle@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
+ integrity sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+ integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+store2@^2.12.0:
+ version "2.13.2"
+ resolved "https://registry.yarnpkg.com/store2/-/store2-2.13.2.tgz#01ad8802ca5b445b9c316b55e72645c13a3cd7e3"
+ integrity sha512-CMtO2Uneg3SAz/d6fZ/6qbqqQHi2ynq6/KzMD/26gTkiEShCcpqFfTHgOxsE0egAq6SX3FmN4CeSqn8BzXQkJg==
+
+storybook-addon-next@^1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/storybook-addon-next/-/storybook-addon-next-1.6.2.tgz#59b2bd007cbc7665ad60c7bdcebad0c65fa3b82f"
+ integrity sha512-v6x2oj+dWqL4Ed64rO8KRQG07JM0uv031IMlBNJAPLclyL7yWA/eCnwiZNNoLOXpV5LCV00KCchF1BJwmqcjBw==
+ dependencies:
+ "@storybook/addons" "^6.4.10"
+ image-size "^1.0.0"
+ loader-utils "^3.2.0"
+ postcss-loader "^6.2.1"
+ resolve-url-loader "^5.0.0"
+ sass-loader "^12.4.0"
+ semver "^7.3.5"
+
+storybook-dark-mode@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/storybook-dark-mode/-/storybook-dark-mode-1.1.0.tgz#4aca307a9c09f1b95743da2db6b07c8eea99ed24"
+ integrity sha512-F+hG02zYGBzxGTUonA1XDV/CtMYm3OjF38Tu1CIUN+w+8hwUrwLcOtgtLLw6VjSrZdJ/ECK+tjXdKTV4oZqAXw==
+ dependencies:
+ fast-deep-equal "^3.0.0"
+ memoizerific "^1.11.3"
+
+stream-browserify@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"
+ integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
+ integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
+ dependencies:
+ end-of-stream "^1.1.0"
+ stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
+ integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.6"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
+ integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
+
string-argv@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
@@ -7629,7 +13976,7 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -7647,7 +13994,7 @@ string-width@^5.0.0:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
-string.prototype.matchall@^4.0.6:
+"string.prototype.matchall@^4.0.0 || ^3.0.1", string.prototype.matchall@^4.0.6:
version "4.0.7"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz#8e6ecb0d8a1fb1fda470d81acecb2dba057a481d"
integrity sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==
@@ -7661,6 +14008,24 @@ string.prototype.matchall@^4.0.6:
regexp.prototype.flags "^1.4.1"
side-channel "^1.0.4"
+string.prototype.padend@^3.0.0:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz#997a6de12c92c7cb34dc8a201a6c53d9bd88a5f1"
+ integrity sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.1"
+
+string.prototype.padstart@^3.0.0:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/string.prototype.padstart/-/string.prototype.padstart-3.1.3.tgz#4551d0117d9501692ec6000b15056ac3f816cfa5"
+ integrity sha512-NZydyOMtYxpTjGqp0VN5PYUF/tsU15yDMZnUdj16qRUIUiMJkHHSDElYyQFrMu+/WloTpA7MQSiADhBicDfaoA==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.1"
+
string.prototype.trimend@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
@@ -7677,7 +14042,7 @@ string.prototype.trimstart@^1.0.4:
call-bind "^1.0.2"
define-properties "^1.1.3"
-string_decoder@^1.1.1:
+string_decoder@^1.0.0, string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
@@ -7725,6 +14090,13 @@ strip-ansi@^7.0.1:
dependencies:
ansi-regex "^6.0.1"
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
+ dependencies:
+ is-utf8 "^0.2.0"
+
strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
@@ -7735,11 +14107,23 @@ strip-bom@^4.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=
+ dependencies:
+ get-stdin "^4.0.1"
+
strip-indent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
@@ -7757,12 +14141,28 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+style-loader@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e"
+ integrity sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==
+ dependencies:
+ loader-utils "^2.0.0"
+ schema-utils "^2.7.0"
+
+style-loader@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
+ integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ==
+ dependencies:
+ loader-utils "^2.0.0"
+ schema-utils "^3.0.0"
+
style-search@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
integrity sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=
-style-to-object@^0.3.0:
+style-to-object@0.3.0, style-to-object@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"
integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==
@@ -7945,6 +14345,21 @@ symbol-tree@^3.2.4:
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+symbol.prototype.description@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/symbol.prototype.description/-/symbol.prototype.description-1.0.5.tgz#d30e01263b6020fbbd2d2884a6276ce4d49ab568"
+ integrity sha512-x738iXRYsrAt9WBhRCVG5BtIC3B7CUkFwbHW2zOvGtwM33s7JjrCDyq8V0zgMYVb5ymsL8+qkzzpANH63CPQaQ==
+ dependencies:
+ call-bind "^1.0.2"
+ get-symbol-description "^1.0.0"
+ has-symbols "^1.0.2"
+ object.getownpropertydescriptors "^2.1.2"
+
+synchronous-promise@^2.0.15:
+ version "2.0.15"
+ resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.15.tgz#07ca1822b9de0001f5ff73595f3d08c4f720eb8e"
+ integrity sha512-k8uzYIkIVwmT+TcglpdN50pS2y1BDcUnBPK9iJeGu0Pl1lOI8pD6wtzgw91Pjpe+RxtTncw32tLxs/R0yNL2Mg==
+
table@^6.8.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca"
@@ -7956,6 +14371,16 @@ table@^6.8.0:
string-width "^4.2.3"
strip-ansi "^6.0.1"
+tapable@^1.0.0, tapable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
+ integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
+
+tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
+ integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
+
tar-fs@^2.0.0, tar-fs@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
@@ -7977,6 +14402,46 @@ tar-stream@^2.1.4:
inherits "^2.0.3"
readable-stream "^3.1.1"
+tar@^6.0.2:
+ version "6.1.11"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
+ integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
+ dependencies:
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ minipass "^3.0.0"
+ minizlib "^2.1.1"
+ mkdirp "^1.0.3"
+ yallist "^4.0.0"
+
+telejson@^5.3.2:
+ version "5.3.3"
+ resolved "https://registry.yarnpkg.com/telejson/-/telejson-5.3.3.tgz#fa8ca84543e336576d8734123876a9f02bf41d2e"
+ integrity sha512-PjqkJZpzEggA9TBpVtJi1LVptP7tYtXB6rEubwlHap76AMjzvOdKX41CxyaW7ahhzDU1aftXnMCx5kAPDZTQBA==
+ dependencies:
+ "@types/is-function" "^1.0.0"
+ global "^4.4.0"
+ is-function "^1.0.2"
+ is-regex "^1.1.2"
+ is-symbol "^1.0.3"
+ isobject "^4.0.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+
+telejson@^6.0.8:
+ version "6.0.8"
+ resolved "https://registry.yarnpkg.com/telejson/-/telejson-6.0.8.tgz#1c432db7e7a9212c1fbd941c3e5174ec385148f7"
+ integrity sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==
+ dependencies:
+ "@types/is-function" "^1.0.0"
+ global "^4.4.0"
+ is-function "^1.0.2"
+ is-regex "^1.1.2"
+ is-symbol "^1.0.3"
+ isobject "^4.0.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+
terminal-link@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994"
@@ -7985,6 +14450,66 @@ terminal-link@^2.0.0:
ansi-escapes "^4.2.1"
supports-hyperlinks "^2.0.0"
+terser-webpack-plugin@^1.4.3:
+ version "1.4.5"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b"
+ integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==
+ dependencies:
+ cacache "^12.0.2"
+ find-cache-dir "^2.1.0"
+ is-wsl "^1.1.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^4.0.0"
+ source-map "^0.6.1"
+ terser "^4.1.2"
+ webpack-sources "^1.4.0"
+ worker-farm "^1.7.0"
+
+terser-webpack-plugin@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz#28daef4a83bd17c1db0297070adc07fc8cfc6a9a"
+ integrity sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ==
+ dependencies:
+ cacache "^15.0.5"
+ find-cache-dir "^3.3.1"
+ jest-worker "^26.5.0"
+ p-limit "^3.0.2"
+ schema-utils "^3.0.0"
+ serialize-javascript "^5.0.1"
+ source-map "^0.6.1"
+ terser "^5.3.4"
+ webpack-sources "^1.4.3"
+
+terser-webpack-plugin@^5.0.3, terser-webpack-plugin@^5.1.3:
+ version "5.3.1"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz#0320dcc270ad5372c1e8993fabbd927929773e54"
+ integrity sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==
+ dependencies:
+ jest-worker "^27.4.5"
+ schema-utils "^3.1.1"
+ serialize-javascript "^6.0.0"
+ source-map "^0.6.1"
+ terser "^5.7.2"
+
+terser@^4.1.2, terser@^4.6.3:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
+ integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
+ dependencies:
+ commander "^2.20.0"
+ source-map "~0.6.1"
+ source-map-support "~0.5.12"
+
+terser@^5.10.0, terser@^5.3.4, terser@^5.7.2:
+ version "5.12.1"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c"
+ integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==
+ dependencies:
+ acorn "^8.5.0"
+ commander "^2.20.0"
+ source-map "~0.7.2"
+ source-map-support "~0.5.20"
+
test-exclude@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
@@ -8029,16 +14554,43 @@ through@2, "through@>=2.2.7 <3", through@^2.3.8:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+timers-browserify@^2.0.4:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee"
+ integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==
+ dependencies:
+ setimmediate "^1.0.4"
+
tmpl@1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+ integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -8046,6 +14598,21 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+toidentifier@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+ integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
totalist@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
@@ -8072,16 +14639,41 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+ integrity sha1-WIeWa7WCpFA6QetST301ARgVphM=
+
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
+trim-trailing-lines@^1.0.0:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz#bd4abbec7cc880462f10b2c8b5ce1d8d1ec7c2c0"
+ integrity sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==
+
+trim@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
+ integrity sha1-WFhUf2spB1fulczMZm+1AITEYN0=
+
+trough@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
+ integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
+
trough@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
+ts-dedent@^2.0.0, ts-dedent@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
+ integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
+
ts-node@^10.7.0:
version "10.7.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5"
@@ -8101,6 +14693,11 @@ ts-node@^10.7.0:
v8-compile-cache-lib "^3.0.0"
yn "3.1.1"
+ts-pnp@^1.1.6:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
+ integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
+
tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
@@ -8111,12 +14708,12 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
-tslib@^1.8.1:
+tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-tslib@^2.1.0:
+tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
@@ -8128,6 +14725,11 @@ tsutils@^3.21.0:
dependencies:
tslib "^1.8.1"
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+ integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -8179,6 +14781,14 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+ integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.24"
+
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@@ -8211,6 +14821,19 @@ unbox-primitive@^1.0.1:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
+unfetch@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
+ integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==
+
+unherit@^1.0.4:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
+ integrity sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==
+ dependencies:
+ inherits "^2.0.0"
+ xtend "^4.0.0"
+
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc"
@@ -8234,6 +14857,18 @@ unicode-property-aliases-ecmascript@^2.0.0:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8"
integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==
+unified@9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8"
+ integrity sha512-vx2Z0vY+a3YoTj8+pttM3tiJHCwY5UFbYdiWrwBEbHmK8pvsPj2rtAX2BFfgXen8T39CJWblWRDT4L5WGXtDdg==
+ dependencies:
+ bail "^1.0.0"
+ extend "^3.0.0"
+ is-buffer "^2.0.0"
+ is-plain-obj "^2.0.0"
+ trough "^1.0.0"
+ vfile "^4.0.0"
+
unified@^10.0.0:
version "10.1.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
@@ -8247,6 +14882,35 @@ unified@^10.0.0:
trough "^2.0.0"
vfile "^5.0.0"
+union-value@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
+ integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^2.0.1"
+
+unique-filename@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+ integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
+ integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==
+ dependencies:
+ imurmurhash "^0.1.4"
+
+unist-builder@2.0.3, unist-builder@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-2.0.3.tgz#77648711b5d86af0942f334397a33c5e91516436"
+ integrity sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==
+
unist-builder@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.0.tgz#728baca4767c0e784e1e64bb44b5a5a753021a04"
@@ -8254,11 +14918,21 @@ unist-builder@^3.0.0:
dependencies:
"@types/unist" "^2.0.0"
+unist-util-generated@^1.0.0:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b"
+ integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==
+
unist-util-generated@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.0.tgz#86fafb77eb6ce9bfa6b663c3f5ad4f8e56a60113"
integrity sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw==
+unist-util-is@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797"
+ integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==
+
unist-util-is@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.1.1.tgz#e8aece0b102fa9bc097b0fef8f870c496d4a6236"
@@ -8271,6 +14945,11 @@ unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0:
dependencies:
"@types/unist" "^2.0.0"
+unist-util-position@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47"
+ integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==
+
unist-util-position@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.3.tgz#5290547b014f6222dff95c48d5c3c13a88fadd07"
@@ -8278,6 +14957,13 @@ unist-util-position@^4.0.0:
dependencies:
"@types/unist" "^2.0.0"
+unist-util-remove-position@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc"
+ integrity sha512-fDZsLYIe2uT+oGFnuZmy73K6ZxOPG/Qcm+w7jbEjaFcJgbQ6cqjs/eSPzXhsmGpAsWPkqZM9pYjww5QTn3LHMA==
+ dependencies:
+ unist-util-visit "^2.0.0"
+
unist-util-remove-position@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.1.tgz#d5b46a7304ac114c8d91990ece085ca7c2c135c8"
@@ -8286,6 +14972,20 @@ unist-util-remove-position@^4.0.0:
"@types/unist" "^2.0.0"
unist-util-visit "^4.0.0"
+unist-util-remove@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/unist-util-remove/-/unist-util-remove-2.1.0.tgz#b0b4738aa7ee445c402fda9328d604a02d010588"
+ integrity sha512-J8NYPyBm4baYLdCbjmf1bhPu45Cr1MWTm77qd9istEkzWpnN6O9tMsEbB2JhNnBCqGENRqEWomQ+He6au0B27Q==
+ dependencies:
+ unist-util-is "^4.0.0"
+
+unist-util-stringify-position@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz#cce3bfa1cdf85ba7375d1d5b17bdc4cada9bd9da"
+ integrity sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==
+ dependencies:
+ "@types/unist" "^2.0.2"
+
unist-util-stringify-position@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.2.tgz#5c6aa07c90b1deffd9153be170dce628a869a447"
@@ -8293,6 +14993,14 @@ unist-util-stringify-position@^3.0.0:
dependencies:
"@types/unist" "^2.0.0"
+unist-util-visit-parents@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6"
+ integrity sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-is "^4.0.0"
+
unist-util-visit-parents@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz#e83559a4ad7e6048a46b1bdb22614f2f3f4724f2"
@@ -8309,6 +15017,15 @@ unist-util-visit-parents@^5.0.0:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
+unist-util-visit@2.0.3, unist-util-visit@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
+ integrity sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-is "^4.0.0"
+ unist-util-visit-parents "^3.0.0"
+
unist-util-visit@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-3.1.0.tgz#9420d285e1aee938c7d9acbafc8e160186dbaf7b"
@@ -8337,6 +15054,31 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+untildify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/untildify/-/untildify-2.1.0.tgz#17eb2807987f76952e9c0485fc311d06a826a2e0"
+ integrity sha1-F+soB5h/dpUunASF/DEdBqgmouA=
+ dependencies:
+ os-homedir "^1.0.0"
+
+upath@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
+ integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==
+
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -8344,6 +15086,28 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url-loader@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2"
+ integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==
+ dependencies:
+ loader-utils "^2.0.0"
+ mime-types "^2.1.27"
+ schema-utils "^3.0.0"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
use-ackee@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/use-ackee/-/use-ackee-3.0.1.tgz#6757c2abea2800369a92eb811ab92d30b67f10d6"
@@ -8351,11 +15115,58 @@ use-ackee@^3.0.1:
dependencies:
ackee-tracker "^5.0.1"
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+util.promisify@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+util@0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+ dependencies:
+ inherits "2.0.1"
+
+util@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
+ integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
+ dependencies:
+ inherits "2.0.3"
+
+utila@~0.4:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
+ integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+uuid-browser@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid-browser/-/uuid-browser-3.1.0.tgz#0f05a40aef74f9e5951e20efbf44b11871e56410"
+ integrity sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA=
+
+uuid@^3.3.2:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
+ integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
+
uvu@^0.5.0:
version "0.5.3"
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.3.tgz#3d83c5bc1230f153451877bfc7f4aea2392219ae"
@@ -8376,7 +15187,7 @@ v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
-v8-to-istanbul@^8.1.0:
+v8-to-istanbul@^8.0.0, v8-to-istanbul@^8.1.0:
version "8.1.1"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz#77b752fd3975e31bbcef938f85e9bd1c7a8d60ed"
integrity sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==
@@ -8393,6 +15204,24 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+
+vfile-location@^3.0.0, vfile-location@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c"
+ integrity sha512-aLEIZKv/oxuCDZ8lkJGhuhztf/BW4M+iHdCwglA/eWc+vtuRFJj8EtgceYFX4LRjOhCAAiNHsKGssC6onJ+jbA==
+
+vfile-message@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
+ integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-stringify-position "^2.0.0"
+
vfile-message@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.2.tgz#a2908f64d9e557315ec9d7ea3a910f658ac05f7d"
@@ -8401,6 +15230,16 @@ vfile-message@^3.0.0:
"@types/unist" "^2.0.0"
unist-util-stringify-position "^3.0.0"
+vfile@^4.0.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624"
+ integrity sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ is-buffer "^2.0.0"
+ unist-util-stringify-position "^2.0.0"
+ vfile-message "^2.0.0"
+
vfile@^5.0.0:
version "5.3.2"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.2.tgz#b499fbc50197ea50ad3749e9b60beb16ca5b7c54"
@@ -8411,6 +15250,11 @@ vfile@^5.0.0:
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
+vm-browserify@^1.0.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
+ integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
+
vue@^3.2.23:
version "3.2.33"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.33.tgz#7867eb16a3293a28c4d190a837bc447878bd64c2"
@@ -8436,13 +15280,44 @@ w3c-xmlserializer@^2.0.0:
dependencies:
xml-name-validator "^3.0.0"
-walker@^1.0.7:
+walker@^1.0.7, walker@~1.0.5:
version "1.0.8"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"
integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==
dependencies:
makeerror "1.0.12"
+watchpack-chokidar2@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"
+ integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==
+ dependencies:
+ chokidar "^2.1.8"
+
+watchpack@^1.7.4:
+ version "1.7.5"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453"
+ integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==
+ dependencies:
+ graceful-fs "^4.1.2"
+ neo-async "^2.5.0"
+ optionalDependencies:
+ chokidar "^3.4.1"
+ watchpack-chokidar2 "^2.0.1"
+
+watchpack@^2.2.0, watchpack@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
+ integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
+ dependencies:
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.1.2"
+
+web-namespaces@^1.0.0:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
+ integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
+
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@@ -8473,6 +15348,166 @@ webpack-bundle-analyzer@4.3.0:
sirv "^1.0.7"
ws "^7.3.1"
+webpack-dev-middleware@^3.7.3:
+ version "3.7.3"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5"
+ integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==
+ dependencies:
+ memory-fs "^0.4.1"
+ mime "^2.4.4"
+ mkdirp "^0.5.1"
+ range-parser "^1.2.1"
+ webpack-log "^2.0.0"
+
+webpack-dev-middleware@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-4.3.0.tgz#179cc40795882cae510b1aa7f3710cbe93c9333e"
+ integrity sha512-PjwyVY95/bhBh6VUqt6z4THplYcsvQ8YNNBTBM873xLVmw8FLeALn0qurHbs9EmcfhzQis/eoqypSnZeuUz26w==
+ dependencies:
+ colorette "^1.2.2"
+ mem "^8.1.1"
+ memfs "^3.2.2"
+ mime-types "^2.1.30"
+ range-parser "^1.2.1"
+ schema-utils "^3.0.0"
+
+webpack-filter-warnings-plugin@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/webpack-filter-warnings-plugin/-/webpack-filter-warnings-plugin-1.2.1.tgz#dc61521cf4f9b4a336fbc89108a75ae1da951cdb"
+ integrity sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==
+
+webpack-hot-middleware@^2.25.1:
+ version "2.25.1"
+ resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.25.1.tgz#581f59edf0781743f4ca4c200fd32c9266c6cf7c"
+ integrity sha512-Koh0KyU/RPYwel/khxbsDz9ibDivmUbrRuKSSQvW42KSDdO4w23WI3SkHpSUKHE76LrFnnM/L7JCrpBwu8AXYw==
+ dependencies:
+ ansi-html-community "0.0.8"
+ html-entities "^2.1.0"
+ querystring "^0.2.0"
+ strip-ansi "^6.0.0"
+
+webpack-log@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f"
+ integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==
+ dependencies:
+ ansi-colors "^3.0.0"
+ uuid "^3.3.2"
+
+webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
+ integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack-sources@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
+ integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
+
+webpack-virtual-modules@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.2.2.tgz#20863dc3cb6bb2104729fff951fbe14b18bd0299"
+ integrity sha512-kDUmfm3BZrei0y+1NTHJInejzxfhtU8eDj2M7OKb2IWrPFAeO1SOH2KuQ68MSZu9IGEHcxbkKKR1v18FrUSOmA==
+ dependencies:
+ debug "^3.0.0"
+
+webpack-virtual-modules@^0.4.1:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.3.tgz#cd597c6d51d5a5ecb473eea1983a58fa8a17ded9"
+ integrity sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==
+
+webpack@4:
+ version "4.46.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542"
+ integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==
+ dependencies:
+ "@webassemblyjs/ast" "1.9.0"
+ "@webassemblyjs/helper-module-context" "1.9.0"
+ "@webassemblyjs/wasm-edit" "1.9.0"
+ "@webassemblyjs/wasm-parser" "1.9.0"
+ acorn "^6.4.1"
+ ajv "^6.10.2"
+ ajv-keywords "^3.4.1"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^4.5.0"
+ eslint-scope "^4.0.3"
+ json-parse-better-errors "^1.0.2"
+ loader-runner "^2.4.0"
+ loader-utils "^1.2.3"
+ memory-fs "^0.4.1"
+ micromatch "^3.1.10"
+ mkdirp "^0.5.3"
+ neo-async "^2.6.1"
+ node-libs-browser "^2.2.1"
+ schema-utils "^1.0.0"
+ tapable "^1.1.3"
+ terser-webpack-plugin "^1.4.3"
+ watchpack "^1.7.4"
+ webpack-sources "^1.4.1"
+
+"webpack@>=4.43.0 <6.0.0":
+ version "5.72.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.72.1.tgz#3500fc834b4e9ba573b9f430b2c0a61e1bb57d13"
+ integrity sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung==
+ dependencies:
+ "@types/eslint-scope" "^3.7.3"
+ "@types/estree" "^0.0.51"
+ "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/wasm-edit" "1.11.1"
+ "@webassemblyjs/wasm-parser" "1.11.1"
+ acorn "^8.4.1"
+ acorn-import-assertions "^1.7.6"
+ browserslist "^4.14.5"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^5.9.3"
+ es-module-lexer "^0.9.0"
+ eslint-scope "5.1.1"
+ events "^3.2.0"
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.2.9"
+ json-parse-even-better-errors "^2.3.1"
+ loader-runner "^4.2.0"
+ mime-types "^2.1.27"
+ neo-async "^2.6.2"
+ schema-utils "^3.1.0"
+ tapable "^2.1.1"
+ terser-webpack-plugin "^5.1.3"
+ watchpack "^2.3.1"
+ webpack-sources "^3.2.3"
+
+webpack@^5.70.0, webpack@^5.9.0:
+ version "5.70.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d"
+ integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==
+ dependencies:
+ "@types/eslint-scope" "^3.7.3"
+ "@types/estree" "^0.0.51"
+ "@webassemblyjs/ast" "1.11.1"
+ "@webassemblyjs/wasm-edit" "1.11.1"
+ "@webassemblyjs/wasm-parser" "1.11.1"
+ acorn "^8.4.1"
+ acorn-import-assertions "^1.7.6"
+ browserslist "^4.14.5"
+ chrome-trace-event "^1.0.2"
+ enhanced-resolve "^5.9.2"
+ es-module-lexer "^0.9.0"
+ eslint-scope "5.1.1"
+ events "^3.2.0"
+ glob-to-regexp "^0.4.1"
+ graceful-fs "^4.2.9"
+ json-parse-better-errors "^1.0.2"
+ loader-runner "^4.2.0"
+ mime-types "^2.1.27"
+ neo-async "^2.6.2"
+ schema-utils "^3.1.0"
+ tapable "^2.1.1"
+ terser-webpack-plugin "^5.1.3"
+ watchpack "^2.3.1"
+ webpack-sources "^3.2.3"
+
whatwg-encoding@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0"
@@ -8513,7 +15548,7 @@ which-boxed-primitive@^1.0.2:
is-string "^1.0.5"
is-symbol "^1.0.3"
-which@^1.3.1:
+which@^1.2.9, which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -8527,13 +15562,20 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
-wide-align@^1.1.0:
+wide-align@^1.1.0, wide-align@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
+widest-line@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
+ integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
+ dependencies:
+ string-width "^4.0.0"
+
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
@@ -8544,6 +15586,20 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+worker-farm@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
+ integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
+ dependencies:
+ errno "~0.1.7"
+
+worker-rpc@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5"
+ integrity sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==
+ dependencies:
+ microevent.ts "~0.1.1"
+
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@@ -8590,6 +15646,18 @@ ws@^7.3.1, ws@^7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67"
integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
+ws@^8.2.3:
+ version "8.5.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
+ integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
+
+x-default-browser@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/x-default-browser/-/x-default-browser-0.4.0.tgz#70cf0da85da7c0ab5cb0f15a897f2322a6bdd481"
+ integrity sha1-cM8NqF2nwKtcsPFaiX8jIqa91IE=
+ optionalDependencies:
+ default-browser-id "^1.0.4"
+
xml-js@^1.6.11:
version "1.6.11"
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
@@ -8607,27 +15675,37 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
-xtend@~4.0.1:
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+y18n@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-yaml@^1.10.0, yaml@^1.10.2:
+yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
-yargs-parser@^20.2.2, yargs-parser@^20.2.3:
+yargs-parser@^20.2.2, yargs-parser@^20.2.3, yargs-parser@^20.2.7:
version "20.2.9"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
@@ -8673,6 +15751,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
+zwitch@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"
+ integrity sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==
+
zwitch@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.2.tgz#91f8d0e901ffa3d66599756dde7f57b17c95dce1"