-
Notifications
You must be signed in to change notification settings - Fork 1
Lyric UI Setup #217
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?
Lyric UI Setup #217
Changes from 12 commits
cde3637
1f96358
4567392
07dac1c
9816b56
a38ecec
52ab73e
1b01b85
a33c78d
2b4fb47
6a4c91d
4372a27
bc67869
8c7c422
2bcc41f
00f8d85
4ed8e1c
575f31f
53bc1e4
1645a25
dca3a23
e72b988
6e8290a
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,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; |
| 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; |
| 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 | ||
| ```--> |
| 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" | ||
| } | ||
| } |
| 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" | ||
| } | ||
| } |
| 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', | ||
| }, | ||
| }; |
| 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 }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import '@/style.css'; | ||
|
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. this import resolves fine internally via this package's own @/* path alias (in tsconfig), but tsc's declaration emit preserves it as is: while most consumers won't notice because |
||
|
|
||
| export { Button, type ButtonProps, buttonVariants } from './components/ui/button'; | ||
| export { cn } from './lib/utils'; | ||
| 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)); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.