From a2f24af1d3b3da33d8a881630442e9420be55664 Mon Sep 17 00:00:00 2001 From: Carsten Milling Date: Sat, 4 Jul 2026 12:35:56 +0200 Subject: [PATCH 1/4] Add CorpusCard component Ports the dracor-frontend CorpusCard to Tailwind CSS + TypeScript. Reuses the existing Commit component and uses TanStack Router's Link for navigation via a consumer-supplied `to` prop. The font-sizes are slightly larger than in dracor-frontend because there Bootstrap sets `:root { font-size: 0.85rem }` making its effective base 13.6px, while Tailwind leaves the root at the browser default of 16px. I will leave it like this for now. When actually migrating dracor-front to use dracor-react components we could decide to either adjust the base font size or stick with the new default. --- .../CorpusCard/CorpusCard.stories.tsx | 95 ++++++++++++ src/components/CorpusCard/CorpusCard.test.tsx | 21 +++ src/components/CorpusCard/CorpusCard.tsx | 144 ++++++++++++++++++ src/components/CorpusCard/index.ts | 1 + src/components/index.ts | 1 + 5 files changed, 262 insertions(+) create mode 100644 src/components/CorpusCard/CorpusCard.stories.tsx create mode 100644 src/components/CorpusCard/CorpusCard.test.tsx create mode 100644 src/components/CorpusCard/CorpusCard.tsx create mode 100644 src/components/CorpusCard/index.ts diff --git a/src/components/CorpusCard/CorpusCard.stories.tsx b/src/components/CorpusCard/CorpusCard.stories.tsx new file mode 100644 index 0000000..88b38cf --- /dev/null +++ b/src/components/CorpusCard/CorpusCard.stories.tsx @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import CorpusCard from './CorpusCard'; + +const meta: Meta = { + title: 'Molecules/CorpusCard', + component: CorpusCard, + tags: ['autodocs'], + parameters: { + router: { + initialEntries: ['/'], + routes: ['/'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + name: 'ger', + title: 'German Drama Corpus', + to: '/', + metrics: { + plays: 704, + characters: 13832, + male: 10982, + female: 2847, + sp: 115234, + stage: 43217, + wordcount: { text: 8248968, sp: 6432100, stage: 1816868 }, + updated: '2024-12-01T10:30:00Z', + }, + }, +}; + +export const WithCommit: Story = { + args: { + name: 'shake', + title: 'Shakespeare Drama Corpus', + to: '/', + commit: 'ae2d0c4d965bf796284334f6b99a529d9e485943', + repo: 'https://github.com/dracor-org/shakedracor', + metrics: { + plays: 37, + characters: 1244, + male: 982, + female: 262, + sp: 31420, + stage: 8934, + wordcount: { text: 884229, sp: 835112, stage: 49117 }, + updated: '2024-11-15T08:00:00Z', + }, + }, +}; + +export const NoGender: Story = { + args: { + name: 'cal', + title: 'Calderón Drama Corpus', + to: '/', + metrics: { + plays: 244, + characters: 1742, + male: 0, + female: 0, + sp: 68900, + stage: 12400, + wordcount: { text: 3200000, sp: 2900000, stage: 300000 }, + updated: '2024-10-20T12:00:00Z', + }, + }, +}; + +export const NeoLatin: Story = { + args: { + name: 'neolat', + title: 'Neo-Latin Drama Corpus', + acronym: 'NeoLatDraCor', + to: '/', + commit: '75eef622ff3e85d2f5d92b4717aae0810283efc9', + repo: 'https://github.com/dracor-org/neolatdracor', + metrics: { + plays: 22, + characters: 320, + male: 226, + female: 62, + sp: 4641, + stage: 609, + wordcount: { text: 125228, sp: 115474, stage: 1712 }, + updated: '2026-05-08T18:47:05Z', + }, + }, +}; diff --git a/src/components/CorpusCard/CorpusCard.test.tsx b/src/components/CorpusCard/CorpusCard.test.tsx new file mode 100644 index 0000000..e2ecb40 --- /dev/null +++ b/src/components/CorpusCard/CorpusCard.test.tsx @@ -0,0 +1,21 @@ +import { render } from '@testing-library/react'; +import { composeStory } from '@storybook/react-vite'; +import Meta, { Default, WithCommit, NoGender } from './CorpusCard.stories'; + +const DefaultCard = composeStory(Default, Meta); +const WithCommitCard = composeStory(WithCommit, Meta); +const NoGenderCard = composeStory(NoGender, Meta); + +describe('CorpusCard', () => { + test('renders default corpus card', () => { + render(); + }); + + test('renders card with commit', () => { + render(); + }); + + test('renders card without gender breakdown', () => { + render(); + }); +}); diff --git a/src/components/CorpusCard/CorpusCard.tsx b/src/components/CorpusCard/CorpusCard.tsx new file mode 100644 index 0000000..5cee5b9 --- /dev/null +++ b/src/components/CorpusCard/CorpusCard.tsx @@ -0,0 +1,144 @@ +import { Link } from '@tanstack/react-router'; +import Commit from '../Commit'; + +export interface Metrics { + plays: number; + characters: number; + male: number; + female: number; + sp?: number; + stage?: number; + wordcount: { + text: number; + sp: number; + stage: number; + }; + updated: string; +} + +export interface Props { + name: string; + title: string; + to: string; + acronym?: string; + commit?: string; + repo?: string; + metrics: Metrics; + locale?: string; +} + +const badge = 'bg-primary text-white px-1.5 py-0.5 rounded-full text-xs'; + +export default function CorpusCard({ + name, + title, + to, + acronym, + commit, + repo, + metrics, + locale = 'en', +}: Props) { + const prefix = acronym + ? acronym.replace('DraCor', '') + : name.charAt(0).toUpperCase() + name.slice(1); + + const fn = (val: number) => + Number(Number.parseFloat(String(val))).toLocaleString(locale); + + return ( +
+ +

+ + {prefix} + + DraCor +

+ +

+ + {title} + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {fn(metrics.plays)} + + Number of plays +
+ {fn(metrics.characters)} +
+ + {metrics.male + metrics.female > 0 + ? `(M: ${metrics.male}, F: ${metrics.female})` + : ''} + +
+ person + {' + '} + personGrp +
+ Number of characters +
+ {fn(metrics.wordcount.text)} + + text +
+ Text tokens +
+ {metrics.sp !== undefined ? fn(metrics.sp) : '—'} +
+ ({fn(metrics.wordcount.sp)}) +
+ sp +
+ (Tokens) +
+ {metrics.stage !== undefined ? fn(metrics.stage) : '—'} +
+ ({fn(metrics.wordcount.stage)}) +
+ stage +
+ (Tokens) +
+ Last update + + {commit && ( + <> + {commit} +
+ + )} + {new Date(metrics.updated).toLocaleString()} +
+
+ ); +} diff --git a/src/components/CorpusCard/index.ts b/src/components/CorpusCard/index.ts new file mode 100644 index 0000000..cf1bf21 --- /dev/null +++ b/src/components/CorpusCard/index.ts @@ -0,0 +1 @@ +export { default } from './CorpusCard'; diff --git a/src/components/index.ts b/src/components/index.ts index 0bdf094..a75603c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,6 +2,7 @@ export { default as ApiDoc } from './ApiDoc'; export { default as AuthorInfo } from './AuthorInfo'; export { default as Authors } from './Authors'; export { default as Commit } from './Commit'; +export { default as CorpusCard } from './CorpusCard'; export { default as DebouncedInput } from './DebouncedInput'; export { default as DocPage } from './DocPage'; export { default as DownloadButton } from './DownloadButton'; From fc811491524611c792b3407a06eaf1ea4d349d90 Mon Sep 17 00:00:00 2001 From: Carsten Milling Date: Sun, 5 Jul 2026 10:39:48 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Renamed=20the=20existing=20branded=20card?= =?UTF-8?q?=20=E2=86=92=20DracorCorpusCard/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CorpusCard/index.ts | 1 - .../DracorCorpusCard.stories.tsx} | 8 ++++---- .../DracorCorpusCard.test.tsx} | 4 ++-- .../DracorCorpusCard.tsx} | 2 +- src/components/DracorCorpusCard/index.ts | 1 + 5 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 src/components/CorpusCard/index.ts rename src/components/{CorpusCard/CorpusCard.stories.tsx => DracorCorpusCard/DracorCorpusCard.stories.tsx} (91%) rename src/components/{CorpusCard/CorpusCard.test.tsx => DracorCorpusCard/DracorCorpusCard.test.tsx} (81%) rename src/components/{CorpusCard/CorpusCard.tsx => DracorCorpusCard/DracorCorpusCard.tsx} (99%) create mode 100644 src/components/DracorCorpusCard/index.ts diff --git a/src/components/CorpusCard/index.ts b/src/components/CorpusCard/index.ts deleted file mode 100644 index cf1bf21..0000000 --- a/src/components/CorpusCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './CorpusCard'; diff --git a/src/components/CorpusCard/CorpusCard.stories.tsx b/src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx similarity index 91% rename from src/components/CorpusCard/CorpusCard.stories.tsx rename to src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx index 88b38cf..3a34914 100644 --- a/src/components/CorpusCard/CorpusCard.stories.tsx +++ b/src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx @@ -1,10 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import CorpusCard from './CorpusCard'; +import DracorCorpusCard from './DracorCorpusCard'; -const meta: Meta = { - title: 'Molecules/CorpusCard', - component: CorpusCard, +const meta: Meta = { + title: 'Molecules/DracorCorpusCard', + component: DracorCorpusCard, tags: ['autodocs'], parameters: { router: { diff --git a/src/components/CorpusCard/CorpusCard.test.tsx b/src/components/DracorCorpusCard/DracorCorpusCard.test.tsx similarity index 81% rename from src/components/CorpusCard/CorpusCard.test.tsx rename to src/components/DracorCorpusCard/DracorCorpusCard.test.tsx index e2ecb40..0c54c3e 100644 --- a/src/components/CorpusCard/CorpusCard.test.tsx +++ b/src/components/DracorCorpusCard/DracorCorpusCard.test.tsx @@ -1,12 +1,12 @@ import { render } from '@testing-library/react'; import { composeStory } from '@storybook/react-vite'; -import Meta, { Default, WithCommit, NoGender } from './CorpusCard.stories'; +import Meta, { Default, WithCommit, NoGender } from './DracorCorpusCard.stories'; const DefaultCard = composeStory(Default, Meta); const WithCommitCard = composeStory(WithCommit, Meta); const NoGenderCard = composeStory(NoGender, Meta); -describe('CorpusCard', () => { +describe('DracorCorpusCard', () => { test('renders default corpus card', () => { render(); }); diff --git a/src/components/CorpusCard/CorpusCard.tsx b/src/components/DracorCorpusCard/DracorCorpusCard.tsx similarity index 99% rename from src/components/CorpusCard/CorpusCard.tsx rename to src/components/DracorCorpusCard/DracorCorpusCard.tsx index 5cee5b9..26237de 100644 --- a/src/components/CorpusCard/CorpusCard.tsx +++ b/src/components/DracorCorpusCard/DracorCorpusCard.tsx @@ -29,7 +29,7 @@ export interface Props { const badge = 'bg-primary text-white px-1.5 py-0.5 rounded-full text-xs'; -export default function CorpusCard({ +export default function DracorCorpusCard({ name, title, to, diff --git a/src/components/DracorCorpusCard/index.ts b/src/components/DracorCorpusCard/index.ts new file mode 100644 index 0000000..8d9ffef --- /dev/null +++ b/src/components/DracorCorpusCard/index.ts @@ -0,0 +1 @@ +export { default } from './DracorCorpusCard'; From e2d3e26ed5d215801725c8220dd0833c28752ee5 Mon Sep 17 00:00:00 2001 From: Carsten Milling Date: Sun, 5 Jul 2026 11:26:36 +0200 Subject: [PATCH 3/4] Add generic CorpusCard for shared use across sibling projects Introduces a composition-based CorpusCard with a CorpusCardRow primitive so eltec-frontend and ecocor-frontend can drop their near-duplicate local implementations. Values passed as numbers are auto-formatted via toLocaleString; an optional popoverContent turns a row's value into a native popover trigger (using useId, no nanoid dep needed). Built-in "Git commit" and "last update" rows render when the corresponding props are set. --- .../CorpusCard/CorpusCard.stories.tsx | 109 ++++++++++++++++++ src/components/CorpusCard/CorpusCard.test.tsx | 31 +++++ src/components/CorpusCard/CorpusCard.tsx | 109 ++++++++++++++++++ src/components/CorpusCard/index.ts | 2 + src/components/index.ts | 3 +- 5 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/components/CorpusCard/CorpusCard.stories.tsx create mode 100644 src/components/CorpusCard/CorpusCard.test.tsx create mode 100644 src/components/CorpusCard/CorpusCard.tsx create mode 100644 src/components/CorpusCard/index.ts diff --git a/src/components/CorpusCard/CorpusCard.stories.tsx b/src/components/CorpusCard/CorpusCard.stories.tsx new file mode 100644 index 0000000..17ca34c --- /dev/null +++ b/src/components/CorpusCard/CorpusCard.stories.tsx @@ -0,0 +1,109 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import CorpusCard, { CorpusCardRow } from './CorpusCard'; + +const meta: Meta = { + title: 'Molecules/CorpusCard', + component: CorpusCard, + tags: ['autodocs'], + parameters: { + router: { + initialEntries: ['/'], + routes: ['/'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'EcoCor Corpus of English Novels', + to: '/', + className: 'w-full md:w-96', + children: ( + <> + + + + + + + ), + }, +}; + +export const WithCommit: Story = { + args: { + title: 'German EcoCor', + to: '/', + className: 'w-full md:w-96', + commit: 'f086cb0ea1f99907d99fba12807e866875ed645d', + repo: 'https://github.com/EcoCor/eco-de', + updated: '2026-05-08T18:47:05Z', + children: ( + <> + + + + + + ), + }, +}; + +export const WithPopover: Story = { + args: { + title: 'ELTeC English Novel Corpus', + to: '/', + className: 'w-full md:w-96', + commit: '639316c845b80f29bc51b805ce7e7230d4ec63a8', + repo: 'https://github.com/clscor-io/ELTeC-eng', + updated: '2026-04-12T09:15:00Z', + children: ( + <> + + Sub scores + + + text + 10 + + + range + 9 + + + female + 11 + + + reprint + 10 + + + + } + /> + + + + + + ), + }, +}; + +export const NoMetrics: Story = { + args: { + title: 'An empty corpus card', + to: '/', + className: 'w-full md:w-96', + }, +}; diff --git a/src/components/CorpusCard/CorpusCard.test.tsx b/src/components/CorpusCard/CorpusCard.test.tsx new file mode 100644 index 0000000..4cdc5f8 --- /dev/null +++ b/src/components/CorpusCard/CorpusCard.test.tsx @@ -0,0 +1,31 @@ +import { render } from '@testing-library/react'; +import { composeStory } from '@storybook/react-vite'; +import Meta, { + Default, + WithCommit, + WithPopover, + NoMetrics, +} from './CorpusCard.stories'; + +const DefaultCard = composeStory(Default, Meta); +const WithCommitCard = composeStory(WithCommit, Meta); +const WithPopoverCard = composeStory(WithPopover, Meta); +const NoMetricsCard = composeStory(NoMetrics, Meta); + +describe('CorpusCard', () => { + test('renders default corpus card', () => { + render(); + }); + + test('renders card with commit and updated', () => { + render(); + }); + + test('renders card with popover row', () => { + render(); + }); + + test('renders card without metrics', () => { + render(); + }); +}); diff --git a/src/components/CorpusCard/CorpusCard.tsx b/src/components/CorpusCard/CorpusCard.tsx new file mode 100644 index 0000000..eb43748 --- /dev/null +++ b/src/components/CorpusCard/CorpusCard.tsx @@ -0,0 +1,109 @@ +import { type ReactNode, useId } from 'react'; +import { Link } from '@tanstack/react-router'; +import Commit from '../Commit'; + +export interface CorpusCardRowProps { + label: ReactNode; + value: ReactNode; + valueClassName?: string; + popoverContent?: ReactNode; + numberLocale?: string; +} + +export function CorpusCardRow({ + label, + value, + valueClassName, + popoverContent, + numberLocale = 'en', +}: CorpusCardRowProps) { + const id = useId(); + const anchorName = `--anchor-${id.replace(/[^a-zA-Z0-9_-]/g, '-')}`; + const content = + typeof value === 'number' ? value.toLocaleString(numberLocale) : value; + const valueClasses = `font-bold ${valueClassName ?? ''}`.trim(); + return ( + + + {popoverContent ? ( + <> + +
+ {popoverContent} +
+ + ) : ( + {content} + )} + + {label} + + ); +} + +export interface CorpusCardProps { + title: string; + to: string; + commit?: string; + repo?: string; + updated?: string; + locale?: string; + className?: string; + children?: ReactNode; +} + +export default function CorpusCard({ + title, + to, + commit, + repo, + updated, + locale, + className, + children, +}: CorpusCardProps) { + const hasFooterRows = Boolean(commit || updated); + const hasTable = Boolean(children) || hasFooterRows; + const wrapperClass = + `rounded-xl inline-block shadow-lg ${className ?? ''}`.trim(); + return ( +
+
+ + {title} + +
+ {hasTable && ( + + + {children} + {commit && ( + {commit}} + /> + )} + {updated && ( + + )} + +
+ )} +
+ ); +} diff --git a/src/components/CorpusCard/index.ts b/src/components/CorpusCard/index.ts new file mode 100644 index 0000000..1b40a30 --- /dev/null +++ b/src/components/CorpusCard/index.ts @@ -0,0 +1,2 @@ +export { default, CorpusCardRow } from './CorpusCard'; +export type { CorpusCardProps, CorpusCardRowProps } from './CorpusCard'; diff --git a/src/components/index.ts b/src/components/index.ts index a75603c..5b6ea77 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,10 +2,11 @@ export { default as ApiDoc } from './ApiDoc'; export { default as AuthorInfo } from './AuthorInfo'; export { default as Authors } from './Authors'; export { default as Commit } from './Commit'; -export { default as CorpusCard } from './CorpusCard'; +export { default as CorpusCard, CorpusCardRow } from './CorpusCard'; export { default as DebouncedInput } from './DebouncedInput'; export { default as DocPage } from './DocPage'; export { default as DownloadButton } from './DownloadButton'; +export { default as DracorCorpusCard } from './DracorCorpusCard'; export { default as IdCopy } from './IdCopy'; export { default as IdLink } from './IdLink'; export { default as LanguageMenu } from './LanguageMenu'; From 2de2b47c849a06abd6771df72420a1aaf0696448 Mon Sep 17 00:00:00 2001 From: Carsten Milling Date: Sun, 5 Jul 2026 11:50:59 +0200 Subject: [PATCH 4/4] Tweak coverage --- src/components/CorpusCard/CorpusCard.stories.tsx | 1 - src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/components/CorpusCard/CorpusCard.stories.tsx b/src/components/CorpusCard/CorpusCard.stories.tsx index 17ca34c..1f65b31 100644 --- a/src/components/CorpusCard/CorpusCard.stories.tsx +++ b/src/components/CorpusCard/CorpusCard.stories.tsx @@ -21,7 +21,6 @@ export const Default: Story = { args: { title: 'EcoCor Corpus of English Novels', to: '/', - className: 'w-full md:w-96', children: ( <> diff --git a/src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx b/src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx index 3a34914..d353b8c 100644 --- a/src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx +++ b/src/components/DracorCorpusCard/DracorCorpusCard.stories.tsx @@ -65,8 +65,6 @@ export const NoGender: Story = { characters: 1742, male: 0, female: 0, - sp: 68900, - stage: 12400, wordcount: { text: 3200000, sp: 2900000, stage: 300000 }, updated: '2024-10-20T12:00:00Z', },