Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cde3637
feat: shadcn and storybook init
JamesTLopez Aug 4, 2026
1f96358
feat: rename button
JamesTLopez Aug 6, 2026
4567392
feat: change to tailwind 4
JamesTLopez Aug 11, 2026
07dac1c
feat: update bundled css naming
JamesTLopez Aug 11, 2026
9816b56
feat: compatible theming with external apps
JamesTLopez Aug 11, 2026
a38ecec
feat: remove dark
JamesTLopez Aug 11, 2026
52ab73e
feat: add readme
JamesTLopez Aug 11, 2026
1b01b85
Merge branch 'main' into feat/lyric-ui-setup
JamesTLopez Aug 11, 2026
a33c78d
fix: build issues
JamesTLopez Aug 11, 2026
2b4fb47
fix: remove animate tailwind
JamesTLopez Aug 11, 2026
6a4c91d
fix: change button location to support shadcn components
JamesTLopez Aug 11, 2026
4372a27
feat: instructions to add components from shad
JamesTLopez Aug 11, 2026
bc67869
fix: update README
JamesTLopez Aug 18, 2026
8c7c422
fix: upgrade all packages to latest and remove outdated
JamesTLopez Aug 20, 2026
2bcc41f
fix: re-add buttons remove default from components.json
JamesTLopez Aug 20, 2026
00f8d85
missing lock
JamesTLopez Aug 20, 2026
4ed8e1c
fix: resolve exporting commonjs
JamesTLopez Aug 25, 2026
575f31f
fix: update components style
JamesTLopez Aug 27, 2026
53bc1e4
feat: add lucide add icon support on button
JamesTLopez Aug 27, 2026
1645a25
Update pnpm-lock.yaml
JamesTLopez Aug 27, 2026
dca3a23
fix: resolve asChild slot issue
JamesTLopez Aug 27, 2026
e72b988
feat: disabled story
JamesTLopez Aug 27, 2026
6e8290a
fix: resolve remove hsl
JamesTLopez Aug 27, 2026
5800ba3
Merge branch 'main' into feat/lyric-ui-setup
JamesTLopez Sep 3, 2026
ba8c8da
refactor: rename lyric-ui package to ui
JamesTLopez Sep 11, 2026
1713b78
fix: remove alias import
JamesTLopez Sep 11, 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
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
11 changes: 11 additions & 0 deletions packages/ui/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { StorybookConfig } from '@storybook/react-vite';

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

export default config;
16 changes: 16 additions & 0 deletions packages/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;
116 changes: 116 additions & 0 deletions packages/ui/README.md
Comment thread
JamesTLopez marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# 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`. shadcn uses HSL to apply their themes by default, this implementation will also be able to support other formats. Ultimately, callers can override the full palette simply by redefining the CSS variables in their own stylesheet which will be explained further 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, it's worth noting that a project also using tailwind may possibly overlap on styles. This won't functionally affect the project, but may cause unintended 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/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": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/style.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
62 changes: 62 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"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": {
"@radix-ui/react-slot": "^1.3.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.33.0",
"tailwind-merge": "^3.6.0"
},
"devDependencies": {
"@storybook/react": "^10.5.9",
"@storybook/react-vite": "^10.5.9",
"@tailwindcss/vite": "^4.3.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"autoprefixer": "^10.5.4",
"postcss": "^8.5.26",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"rimraf": "^6.1.3",
"storybook": "^10.5.9",
"tailwindcss": "^4.3.3",
"vite": "^8.2.1"
}
}
77 changes: 77 additions & 0 deletions packages/ui/src/components/ui/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Download } from 'lucide-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 DefaultWithIcon: Story = {
args: {
variant: 'default',
icon: <Download />,
},
};

export const Disabled: Story = {
args: {
variant: 'default',
disabled: true,
},
};

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',
},
};
57 changes: 57 additions & 0 deletions packages/ui/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

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

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

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
icon?: React.ReactNode;
iconPosition?: 'left' | 'right';
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, icon, iconPosition = 'left', children, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';

return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props}>
<>
{!asChild && icon && iconPosition === 'left' && icon}
{size !== 'icon' ? children : null}
{!asChild && icon && iconPosition === 'right' && icon}
</>
</Comp>
);
},
);
Button.displayName = 'Button';

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

export { Button, type ButtonProps, buttonVariants } from './components/ui/button';
export { cn } from './lib/utils';
6 changes: 6 additions & 0 deletions packages/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