Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0a34220
Feat(docsite): Display component list
literat Jun 4, 2025
0c3057b
test(docsite): setup unit test workflow and scripts
literat Mar 25, 2026
43f91ef
refactor(docsite): use url slug to directory and back translation for…
literat Mar 25, 2026
e4222d6
feat(docsite): introduce categorical and alphabetical sorting of comp…
literat Mar 25, 2026
9eaf892
style(docsite): fix missing build while type-check on ci
literat Mar 25, 2026
d4725ff
docs(repo): enhanced CLAUDE.md about yarn catalogs feature
literat Mar 25, 2026
fe6e5a3
chore(web-react): defined dependency on design tokens while type chec…
literat Mar 25, 2026
26829ff
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
2adb1f0
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
f572b8e
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
daa8465
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
99b7096
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
d081ab1
fixup! refactor(docsite): use url slug to directory and back translat…
literat Mar 31, 2026
51bc272
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
9f5b270
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
0d215ff
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
033cfc9
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
59f17bc
fixup! feat(docsite): introduce categorical and alphabetical sorting …
literat Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ catalogs:
'@csstools/normalize.css': ^12.0.0
html-dom-parser: 5.0.13
html-react-parser: 5.1.1
nuqs: 2.8.9
# Packages used for scripting tasks
script:
cross-env: 10.1.0
Expand Down
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Spirit Design System is an open-source design system developed by Alma Career (f

- This project is using `yarn` as the package manager.
Use `yarn` commands for installing dependencies, running scripts, and managing packages. Do not use `npm` or `pnpm`.
- This project uses **Yarn Catalogs** for centralized dependency version management.
Never use `yarn add <package>` directly — it bypasses the catalog system. Instead:
1. Add the version to the appropriate catalog in `.yarnrc.yml` under the `catalogs:` section.
2. Reference it in `package.json` as `"<package>": "catalog:<catalog-name>"`.
3. Run `yarn install`.
- When starting working with the codebase, refer to the
[Getting Started section of the Development Guide][development-guide-getting-started] for base tooling
and repository management.
Expand Down
10 changes: 10 additions & 0 deletions apps/docsite/jest.config.ts
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;
12 changes: 11 additions & 1 deletion apps/docsite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
"build": "next build",
"start": "next start",
"lint": "eslint",
"lint:fix": "yarn lint --fix"
"lint:fix": "yarn lint --fix",
"test": "npm-run-all --serial lint test:unit:coverage",
"test:unit": "TS_NODE_PROJECT=./tsconfig.test.json jest",
"test:unit:watch": "yarn test:unit --watchAll",
"test:unit:coverage": "yarn test:unit --coverage",
"types": "tsc"
},
"dependencies": {
"@alma-oss/spirit-design-tokens": "workspace:^",
Expand All @@ -17,6 +22,7 @@
"@alma-oss/spirit-web-react": "workspace:^",
"classnames": "catalog:frontend",
"next": "catalog:frontend",
"nuqs": "catalog:frontend",
"react": "catalog:frontend",
"react-dom": "catalog:frontend",
"sass-embedded": "catalog:build"
Expand All @@ -33,8 +39,12 @@
"eslint-config-next": "catalog:lint",
"eslint-config-spirit": "workspace:^",
"globals": "catalog:lint",
"jest": "catalog:test",
"jest-config-spirit": "workspace:^",
"npm-run-all2": "catalog:script",
"postcss": "catalog:build",
"postcss-preset-env": "catalog:build",
"ts-node": "catalog:script",
"typescript": "catalog:types"
}
}
3 changes: 3 additions & 0 deletions apps/docsite/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
},
"lint": {
"dependsOn": ["@alma-oss/spirit-icons:build"]
},
"types": {
"dependsOn": ["^build"]
}
},
"implicitDependencies": [
Expand Down
23 changes: 23 additions & 0 deletions apps/docsite/src/app/(documentation)/components/page.tsx
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;
14 changes: 6 additions & 8 deletions apps/docsite/src/app/(documentation)/page.tsx
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 />
</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);
});
});
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'],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interactive tag is action too 😂

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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),
);
});

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;
};
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;
26 changes: 26 additions & 0 deletions apps/docsite/src/domains/components/ui/ComponentCard.tsx
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>
</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;
12 changes: 12 additions & 0 deletions apps/docsite/src/domains/components/ui/ComponentGrid.tsx
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;
Loading
Loading