-
Notifications
You must be signed in to change notification settings - Fork 2
Feat(docsite): Display component list #2552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0a34220
0c3057b
43f91ef
e4222d6
9eaf892
d4725ff
fe6e5a3
26829ff
2adb1f0
f572b8e
daa8465
99b7096
d081ab1
51bc272
9f5b270
0d215ff
033cfc9
59f17bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| /** @jest-config-loader ts-node */ | ||
|
|
||
| const config = { | ||
| preset: 'jest-config-spirit', | ||
| moduleNameMapper: { | ||
| '^@local/(.*)': '<rootDir>/src/$1', | ||
| }, | ||
| }; | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { Flex, Section } from '@alma-oss/spirit-web-react'; | ||
| import { fetchAllComponents } from '@local/domains/components/repositories/componentsRepository'; | ||
| import ComponentList from '@local/domains/components/ui/ComponentList'; | ||
| import ComponentListSkeleton from '@local/domains/components/ui/ComponentListSkeleton'; | ||
| import ComponentSortToggle from '@local/domains/components/ui/ComponentSortToggle'; | ||
| import React, { Suspense } from 'react'; | ||
|
|
||
| const ComponentsPage = () => { | ||
| const components: string[] = fetchAllComponents(); | ||
|
|
||
| return ( | ||
| <Section hasContainer> | ||
| <Flex alignmentX="center" marginBottom="space-1200"> | ||
| <ComponentSortToggle /> | ||
| </Flex> | ||
| <Suspense fallback={<ComponentListSkeleton />}> | ||
| <ComponentList components={components} /> | ||
| </Suspense> | ||
| </Section> | ||
| ); | ||
| }; | ||
|
|
||
| export default ComponentsPage; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,13 @@ | ||
| import { Container, Grid, Section } from '@alma-oss/spirit-web-react'; | ||
| import { Grid, Section } from '@alma-oss/spirit-web-react'; | ||
| import ComponentsCard from '@local/domains/homepage/ComponentsCard'; | ||
| import { NextPage } from 'next'; | ||
|
|
||
| const Home: NextPage = () => ( | ||
| <Container> | ||
| <Section> | ||
| <Grid cols={{ mobile: 1, tablet: 2 }}> | ||
| <ComponentsCard /> | ||
| </Grid> | ||
| </Section> | ||
| </Container> | ||
| <Section hasContainer> | ||
| <Grid cols={{ mobile: 1, tablet: 2 }}> | ||
| <ComponentsCard /> | ||
literat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </Grid> | ||
| </Section> | ||
| ); | ||
|
|
||
| export default Home; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { COMPONENT_CATEGORIES } from '../componentCategories'; | ||
|
|
||
| describe('componentCategories', () => { | ||
| it('should have no duplicate components across categories', () => { | ||
| const allCategorized = Object.values(COMPONENT_CATEGORIES).flat(); | ||
| const uniqueComponents = new Set(allCategorized); | ||
|
|
||
| expect(allCategorized).toHaveLength(uniqueComponents.size); | ||
| }); | ||
|
|
||
| it('should have no empty categories', () => { | ||
| for (const [, components] of Object.entries(COMPONENT_CATEGORIES)) { | ||
| expect(components.length).toBeGreaterThan(0); | ||
| } | ||
| }); | ||
|
|
||
| it('should have components sorted alphabetically within each category', () => { | ||
| for (const [, components] of Object.entries(COMPONENT_CATEGORIES)) { | ||
| const sorted = [...components].sort(); | ||
|
|
||
| expect(components).toEqual(sorted); | ||
| } | ||
| }); | ||
|
|
||
| it('should have categories sorted alphabetically', () => { | ||
| const categories = Object.keys(COMPONENT_CATEGORIES); | ||
| const sorted = [...categories].sort(); | ||
|
|
||
| expect(categories).toEqual(sorted); | ||
| }); | ||
| }); |
literat marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| export const SORT_OPTIONS = { | ||
| ALPHABETICAL: 'alphabetical', | ||
| CATEGORICAL: 'categorical', | ||
| } as const; | ||
|
|
||
| export type SortOption = (typeof SORT_OPTIONS)[keyof typeof SORT_OPTIONS]; | ||
|
|
||
| export const COMPONENT_CATEGORIES: Record<string, string[]> = { | ||
| Actions: ['ActionGroup', 'Button', 'ButtonLink', 'ControlButton', 'Link', 'SkipLink', 'SplitButton'], | ||
| Content: ['Accordion', 'Avatar', 'Card', 'EmptyState', 'Item', 'Pill', 'PricingPlan', 'Tag', 'Timeline'], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interactive tag is action too 😂
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can suggest different categories. And I think this will definitely change in the future. So maybe we can have a long topic about this... :-) |
||
| Feedback: ['Alert', 'Skeleton', 'Spinner', 'Toast', 'Tooltip'], | ||
| Forms: [ | ||
| 'Checkbox', | ||
| 'Field', | ||
| 'FieldGroup', | ||
| 'FileUploader', | ||
| 'Radio', | ||
| 'Select', | ||
| 'Slider', | ||
| 'TextArea', | ||
| 'TextField', | ||
| 'TextFieldBase', | ||
| 'Toggle', | ||
| 'UNSTABLE_Attachment', | ||
| 'UNSTABLE_FileUpload', | ||
| ], | ||
| Layout: ['Box', 'Collapse', 'Container', 'Divider', 'Flex', 'Grid', 'Matrix', 'ScrollView', 'Section', 'Stack'], | ||
| 'Media and Icons': ['Icon', 'IconBox', 'PartnerLogo', 'ProductLogo'], | ||
| Navigation: ['Breadcrumbs', 'Dropdown', 'Navigation', 'Pagination', 'SegmentedControl', 'Tabs'], | ||
| Overlays: ['Dialog', 'Drawer', 'Modal'], | ||
| Structure: ['Footer', 'Header', 'UNSTABLE_Header'], | ||
| Typography: ['Heading', 'Text'], | ||
| Utilities: ['Hidden', 'NoSsr', 'Truncate', 'VisuallyHidden'], | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| 'use client'; | ||
|
|
||
| import { SORT_OPTIONS, type SortOption } from '@local/domains/components/constants/componentCategories'; | ||
| import { useQueryState, parseAsStringEnum } from 'nuqs'; | ||
|
|
||
| const componentsSortParser = parseAsStringEnum<SortOption>([ | ||
| SORT_OPTIONS.ALPHABETICAL, | ||
| SORT_OPTIONS.CATEGORICAL, | ||
| ]).withDefault(SORT_OPTIONS.ALPHABETICAL); | ||
|
|
||
| export const useComponentsSortQueryState = () => useQueryState('sort', componentsSortParser); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { type Dirent, readdirSync } from 'fs'; | ||
| import { fetchAllComponents } from '../componentsRepository'; | ||
|
|
||
| jest.mock('fs', () => ({ | ||
| readdirSync: jest.fn(), | ||
| })); | ||
|
|
||
| const readdirSyncMock = readdirSync as jest.Mock; | ||
|
|
||
| const makeDirent = (name: string, isDir: boolean): Dirent => | ||
| ({ | ||
| name, | ||
| isDirectory: () => isDir, | ||
| isFile: () => !isDir, | ||
| }) as unknown as Dirent; | ||
|
|
||
| describe('fetchAllComponents', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('should return only directory names', () => { | ||
| readdirSyncMock.mockReturnValue([ | ||
| makeDirent('Accordion', true), | ||
| makeDirent('Button', true), | ||
| makeDirent('.DS_Store', false), | ||
| makeDirent('index.ts', false), | ||
| ]); | ||
|
|
||
| const result = fetchAllComponents(); | ||
|
|
||
| expect(result).toEqual(['Accordion', 'Button']); | ||
| }); | ||
|
|
||
| it('should return an empty array when no directories exist', () => { | ||
| readdirSyncMock.mockReturnValue([]); | ||
|
|
||
| const result = fetchAllComponents(); | ||
|
|
||
| expect(result).toEqual([]); | ||
| }); | ||
|
|
||
| it('should call readdirSync with withFileTypes option', () => { | ||
| readdirSyncMock.mockReturnValue([]); | ||
|
|
||
| fetchAllComponents(); | ||
|
|
||
| expect(readdirSyncMock).toHaveBeenCalledWith(expect.any(String), { withFileTypes: true }); | ||
| }); | ||
|
|
||
| it('should resolve the path to web-react components directory', () => { | ||
| readdirSyncMock.mockReturnValue([]); | ||
|
|
||
| fetchAllComponents(); | ||
|
|
||
| expect(readdirSyncMock).toHaveBeenCalledWith( | ||
| expect.stringContaining('packages/web-react/src/components'), | ||
| expect.any(Object), | ||
| ); | ||
literat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| it('should propagate errors when the directory cannot be read', () => { | ||
| readdirSyncMock.mockImplementation(() => { | ||
| throw new Error('ENOENT'); | ||
| }); | ||
|
|
||
| expect(() => fetchAllComponents()).toThrow('ENOENT'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { readdirSync } from 'fs'; | ||
| import { resolve } from 'path'; | ||
|
|
||
| const getDirs = (source: string) => | ||
| readdirSync(source, { withFileTypes: true }) | ||
| .filter((dirent) => dirent.isDirectory()) | ||
| .map((dirent) => dirent.name); | ||
|
|
||
| export const fetchAllComponents = (): string[] => { | ||
| const components = getDirs(resolve(process.cwd(), '../../packages/web-react/src/components')); | ||
|
|
||
| return components; | ||
literat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import ComponentGrid from '@local/domains/components/ui/ComponentGrid'; | ||
| import React from 'react'; | ||
| import ComponentCard from './ComponentCard'; | ||
|
|
||
| interface AlphabeticalComponentListProps { | ||
| components: string[]; | ||
| } | ||
|
|
||
| const AlphabeticalComponentList = ({ components }: AlphabeticalComponentListProps) => { | ||
| const sorted = [...components].sort(); | ||
|
|
||
| return ( | ||
| <ComponentGrid> | ||
| {sorted.map((component) => ( | ||
| <ComponentCard key={component} component={component} /> | ||
| ))} | ||
| </ComponentGrid> | ||
| ); | ||
| }; | ||
|
|
||
| export default AlphabeticalComponentList; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { Heading, Section } from '@alma-oss/spirit-web-react'; | ||
| import ComponentGrid from '@local/domains/components/ui/ComponentGrid'; | ||
| import { groupComponentsByCategory } from '@local/domains/components/utils/groupComponentsByCategory'; | ||
| import React from 'react'; | ||
| import ComponentCard from './ComponentCard'; | ||
|
|
||
| interface CategoricalComponentListProps { | ||
| components: string[]; | ||
| } | ||
|
|
||
| const CategoricalComponentList = ({ components }: CategoricalComponentListProps) => { | ||
| const categories = groupComponentsByCategory(components); | ||
|
|
||
| return ( | ||
| <> | ||
| {categories.map(({ category, components: filtered }) => ( | ||
| <Section key={category} marginBottom="space-1200" hasContainer={false}> | ||
| <Heading elementType="h2" marginBottom="space-800"> | ||
| {category} | ||
| </Heading> | ||
| <ComponentGrid> | ||
| {filtered.map((component) => ( | ||
| <ComponentCard key={component} component={component} /> | ||
| ))} | ||
| </ComponentGrid> | ||
| </Section> | ||
| ))} | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default CategoricalComponentList; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| 'use client'; | ||
|
|
||
| import { Card, CardBody, CardLink, CardTitle } from '@alma-oss/spirit-web-react'; | ||
| import { routes } from '@local/domains/routing/routes'; | ||
| import NextLink from 'next/link'; | ||
| import React from 'react'; | ||
|
|
||
| interface ComponentCardProps { | ||
| component: string; | ||
| } | ||
|
|
||
| const ComponentCard = ({ component }: ComponentCardProps) => ( | ||
| <li className="d-grid"> | ||
| <Card direction="horizontal" isBoxed> | ||
| <CardBody> | ||
| <CardTitle isHeading> | ||
| <CardLink elementType={NextLink} href={routes.component(component)}> | ||
| {component} | ||
| </CardLink> | ||
literat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </CardTitle> | ||
| </CardBody> | ||
| </Card> | ||
| </li> | ||
| ); | ||
|
|
||
| export default ComponentCard; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { SkeletonShape } from '@alma-oss/spirit-web-react'; | ||
| import React from 'react'; | ||
|
|
||
| const ComponentCardSkeleton = () => ( | ||
| <li className="d-grid"> | ||
| <SkeletonShape width={0} height={66} borderRadius="200" UNSAFE_style={{ width: '100%' }} /> | ||
| </li> | ||
| ); | ||
|
|
||
| export default ComponentCardSkeleton; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { ChildrenProps, Grid } from '@alma-oss/spirit-web-react'; | ||
| import React from 'react'; | ||
|
|
||
| interface ComponentGridProps extends ChildrenProps {} | ||
|
|
||
| const ComponentGrid = ({ children }: ComponentGridProps) => ( | ||
| <Grid elementType="ul" cols={{ mobile: 2, tablet: 3 }}> | ||
| {children} | ||
| </Grid> | ||
| ); | ||
|
|
||
| export default ComponentGrid; |
Uh oh!
There was an error while loading. Please reload this page.