Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ out
.nuxt
dist

# Storybook build output
storybook-static

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
Expand Down
12 changes: 12 additions & 0 deletions packages/lyric-ui/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|tsx)'],
addons: ['@storybook/addon-essentials'],
framework: {
name: '@storybook/react-vite',
options: {},
},
};

export default config;
16 changes: 16 additions & 0 deletions packages/lyric-ui/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import '@/style.css';

import type { Preview } from '@storybook/react';

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export default preview;
117 changes: 117 additions & 0 deletions packages/lyric-ui/README.md
Comment thread
JamesTLopez marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# lyric-ui

Shared React component library for Lyric-based UIs.

`lyric-ui` provides a set of accessible, themeable UI primitives for building front-end applications that interact with the [Lyric](https://github.com/overture-stack/lyric) data submission system.

---

## Features

- **Themeable** — Theming can be customized using css variables in the main .css file in your application. Please see section [Theming].
- **Variant-driven components** — built with [`class-variance-authority`](https://cva.style/) so each component exposes a clean, type-safe variant API.
- **Dual-format build** — ships both ES module (`lyric-ui.js`) and CommonJS (`lyric-ui.cjs`) outputs so it works in both modern bundlers and legacy setups.
- **Storybook** — every component has stories for interactive development and visual documentation.

---

### Utilities

| Export | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------- |
| `cn(...inputs)` | Merges Tailwind class strings, resolving conflicts via `tailwind-merge` and conditional logic via `clsx`. |
| `buttonVariants` | The underlying CVA variant factory for `Button`, useful when you need the class string without the component. |

---

## Technologies

| Technology | Role |
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| [React 18](https://react.dev/) | Component runtime |
| [TypeScript](https://www.typescriptlang.org/) | Authoring language; types shipped in `dist/` |
| [Tailwind CSS v4](https://tailwindcss.com/) | Utility-first styling |
| [class-variance-authority](https://cva.style/) | Variant and slot management for components |
| [clsx](https://github.com/lukeed/clsx) + [tailwind-merge](https://github.com/dcastil/tailwind-merge) | Conditional class composition without conflicts |
| [Vite](https://vitejs.dev/) | Library build (ESM + CJS, bundled CSS) |
| [Storybook 8](https://storybook.js.org/) | Component development environment and visual docs |

The theming system follows a similar [shadcn/ui](https://ui.shadcn.com/) convention: design tokens are declared as CSS custom properties in a `@layer base` block, then referenced by Tailwind via `@theme`. Shadcdn uses HSL to apply their themes by default, this implementation will also be able to support other formats. Ultimatly, callers can override the full palette simply by redefining the CSS variables in their own stylesheet which will be explained futher in the next section.

### Theming

Tailoring lyric-ui components to a target application is done by overriding CSS variables in your application's main `.css` file. The following tokens control the full palette:

| Variable | Default value | Description |
| -------------------------- | ------------------------ | ------------------------------------------------------------------- |
| `--background` | `hsl(0 0% 100%)` | Page/surface background |
| `--foreground` | `hsl(222.2 84% 4.9%)` | Default text and icon color |
| `--primary` | `hsl(222.2 47.4% 11.2%)` | Primary action color (buttons, links) |
| `--primary-foreground` | `hsl(210 40% 98%)` | Text/icons rendered on a primary-colored surface |
| `--secondary` | `hsl(210 40% 96.1%)` | Secondary action or surface color |
| `--secondary-foreground` | `hsl(222.2 47.4% 11.2%)` | Text/icons rendered on a secondary-colored surface |
| `--muted` | `hsl(210 40% 96.1%)` | Subdued background for non-interactive areas (badges, placeholders) |
| `--muted-foreground` | `hsl(215.4 16.3% 46.9%)` | Text/icons rendered on a muted surface |
| `--accent` | `hsl(210 40% 96.1%)` | Highlight or hover state background |
| `--accent-foreground` | `hsl(222.2 47.4% 11.2%)` | Text/icons rendered on an accent-colored surface |
| `--destructive` | `hsl(0 84.2% 60.2%)` | Destructive/error action color (delete, error states) |
| `--destructive-foreground` | `hsl(210 40% 98%)` | Text/icons rendered on a destructive-colored surface |
| `--border` | `hsl(214.3 31.8% 91.4%)` | Default border color for cards, inputs, and dividers |
| `--input` | `hsl(214.3 31.8% 91.4%)` | Input field border color |
| `--ring` | `hsl(222.2 84% 4.9%)` | Focus ring color for interactive elements |
| `--radius` | `0.5rem` | Base border-radius used across components |

Your main css file should look something like this:

```css
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--primary: hsl(222.2 47.4% 11.2%);
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(210 40% 96.1%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(210 40% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(222.2 84% 4.9%);
--radius: 0.5rem;
}
```

If you are using tailwind along side with lyric-ui, you can use tailwind css variable to populate lyric-ui as a source of truth:

```css
@theme {
--color-primary-900: #054a74;
}

:root {
--primary: var(--color-primary-900);
}
```

Because tailwind is being bundled with these exported components, its worth noting that a project also using tailwind may possible overlap on styles. Functionally will not effect
the project but may encounter unintented visual issues.

---

## Adding new components from Shadcn

From the component library from [shadcn](https://ui.shadcn.com/docs/components), select targeted component and run the command they give you for that component.
For example, `pnpm dlx shadcn@latest add card` will add a card component. All components added through this method will be added to the path `components/ui/*`.

NOTE: Some components may also install third-party libraries.
For example `pnpm dlx shadcn@latest add popover` will add `@radix-ui/react-popover`.

## Installation

<!--
```bash
pnpm add @overture-stack/lyric-ui
```-->
16 changes: 16 additions & 0 deletions packages/lyric-ui/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/style.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
61 changes: 61 additions & 0 deletions packages/lyric-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "@overture-stack/lyric-ui",
"version": "0.0.0-dev",
"description": "Shared React component library for Lyric-based UIs.",
"type": "module",
"main": "./dist/lyric-ui.cjs",
"module": "./dist/lyric-ui.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/lyric-ui.js",
"require": "./dist/lyric-ui.cjs"
},
"./dist/lyric-ui.css": "./dist/lyric-ui.css"
},
"files": [
"dist/"
],
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"nuke:build": "npx rimraf dist",
"build:all": "pnpm nuke:build && vite build && tsc -p tsconfig.build.json",
"storybook": "storybook dev -p 6006",
"build:storybook": "storybook build"
},
"author": "Ontario Institute for Cancer Research",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "https://github.com/overture-stack/lyric.git"
},
"peerDependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"dependencies": {
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"tailwind-merge": "^2.5.2"
},
"devDependencies": {
"@storybook/addon-essentials": "^8.3.5",
"@storybook/react": "^8.3.5",
"@storybook/react-vite": "^8.3.5",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"@tailwindcss/vite": "^4.3.3",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.45",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rimraf": "^5.0.9",
"storybook": "^8.3.5",
"tailwindcss": "^4.0.0",
"vite": "^7.1.2"
}
}
62 changes: 62 additions & 0 deletions packages/lyric-ui/src/components/ui/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Button } from './button';

const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link'],
},
size: {
control: 'select',
options: ['default', 'sm', 'lg', 'icon'],
},
},
args: {
children: 'Button',
},
};

export default meta;

type Story = StoryObj<typeof Button>;

export const Default: Story = {
args: {
variant: 'default',
},
};

export const Secondary: Story = {
args: {
variant: 'secondary',
},
};

export const Destructive: Story = {
args: {
variant: 'destructive',
},
};

export const Outline: Story = {
args: {
variant: 'outline',
},
};

export const Ghost: Story = {
args: {
variant: 'ghost',
},
};

export const Link: Story = {
args: {
variant: 'link',
},
};
39 changes: 39 additions & 0 deletions packages/lyric-ui/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { cva, type VariantProps } from 'class-variance-authority';
import { type ButtonHTMLAttributes, forwardRef } from 'react';

import { cn } from '@/lib/utils';

const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, ...props }, ref) => {
return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
});
Button.displayName = 'Button';

export { Button, buttonVariants };
4 changes: 4 additions & 0 deletions packages/lyric-ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import '@/style.css';

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.

this import resolves fine internally via this package's own @/* path alias (in tsconfig), but tsc's declaration emit preserves it as is: dist/index.d.ts ships with import '@/style.css'; still in it... that alias means nothing to an external consumer, and doesn't even match the real output filename (lyric-ui.css, not style.css)... needs transformers.

while most consumers won't notice because skipLibCheck: true is a common default, this is a real defect in the published artifact. the way we handle this elsewhere is like so


export { Button, type ButtonProps, buttonVariants } from './components/ui/button';
export { cn } from './lib/utils';
6 changes: 6 additions & 0 deletions packages/lyric-ui/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Loading