npm package: @podchaser/component-library
Description: This is podchaser component library.
Author: Darryl Brown
Latest version: 1.24.0

@podchaser/component-library

A typed React component library for Podchaser. Built with TypeScript, styled-components, Rollup, and Storybook. Ships dual bundles (ESM + CJS) and type definitions.

Goal: unify Podchaser’s UI with reusable, token-driven components and faster delivery.

⸻

Why this exists

This library is the single source of truth for Podchaser UI:
	•	Consistent visuals across apps (design tokens, theming).
	•	Reusable, well-typed components to speed up feature work.
	•	Migration path away from ad-hoc styles toward styled-components + Storybook.

High-level initiative details live in the repo as context for the team (purpose, rollout, testing, etc.).

⸻

Install

# npm
npm i @podchaser/component-library

# or yarn
yarn add @podchaser/component-library

# or pnpm
pnpm add @podchaser/component-library

Peer deps
	•	React: 19.2.0 (you must provide React in the consuming app)
	•	React DOM: 19.2.0
	•	styled-components: ^6.1.19 (used for all component styling)

⸻

## Usage

### Button

```tsx
// ESM (recommended)
import { StandardButton } from "@podchaser/component-library";

export function Example() {
  return (
    <StandardButton
      label="Primary CTA"
      variation="primary"
      size="md"
      onClick={() => {
        // handle click
      }}
    />
  );
}
```

You can also render it as a link:

```tsx
// As an anchor
<StandardButton href="https://podchaser.com" target="_blank" rel="noreferrer">
  Visit Podchaser
</StandardButton>

// With a router Link (e.g. react-router-dom)
import { Link } from "react-router-dom";

<StandardButton linkComponent={Link} to="/dashboard">
  Go to dashboard
</StandardButton>
```

#### Props

| Prop          | Type                                               | Default      | Description                                                                                           |
| ------------- | -------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------- |
| `label`       | `ReactNode`                                       | `"Click Me"` | Primary content shown inside the button. Falls back to `title` and then `children`.                  |
| `title`       | `ReactNode`                                       | `undefined`  | Backwards‑compatible alias for `label`.                                                              |
| `hoverLabel`  | `ReactNode`                                       | `undefined`  | Optional alternate label shown only while the button is hovered.                                     |
| `variation`   | `"primary" \| "transparent" \| "actioned" \| "clear" \| "blue" \| "green" \| "red" \| "pink" \| "negative" \| "white" \| "whiteWithHoverBorder" \| "whiteNegative" \| "subtle" \| "darkSubtle" \| "darkSuperSubtle" \| "none"` | `"primary"` | Visual variation style of the button.                                                                |
| `size`        | `"sm" \| "md" \| "lg"`                            | `"md"`       | Size preset controlling padding and base font‑size (≈12px by default).                              |
| `fullWidth`   | `boolean`                                         | `false`      | When `true`, button stretches to fill the width of its container.                                    |
| `width`       | `string`                                          | `"max-content"` | Explicit width (e.g. `"240px"`, `"100%"`). Overrides `fullWidth` logic when set.                  |
| `height`      | `string`                                          | `"2rem"`     | Explicit height (e.g. `"40px"`).                                                                     |
| `padding`     | `string`                                          | size preset  | Padding override (e.g. `"0.5rem 1rem"`), applied on top of the chosen `size`.                       |
| `margin`      | `string`                                          | `0`          | Margin override around the button (e.g. `"0 0 1rem 0"`).                                            |
| `justifyContent` | `string`                                       | `"center"`   | Flex `justify-content` for the inner button content (e.g. `"center"`, `"flex-start"`).              |
| `alignItems`  | `string`                                          | `"center"`   | Flex `align-items` for the inner button content (e.g. `"center"`, `"flex-start"`).                  |
| `borderRadius`| `string`                                          | `"25px"`     | Border radius override (e.g. `"4px"`, `"25px"`).                                                     |
| `borderColor` | `string`                                          | `transparent`| Border color override.                                                                               |
| `hoveredBorderColor` | `string`                                   | border       | Border color to use on hover. Falls back to `borderColor` when not provided.                        |
| `background`  | `string`                                          | variation    | Full CSS `background` value (e.g. gradients like `"linear-gradient(...)"`). Takes precedence over variation backgrounds. |
| `backgroundColor` | `string`                                      | variation    | Background color override. When set, it takes precedence over `variation` (unless `background` is set). |
| `hoveredBackgroundColor` | `string`                               | background   | Background color to use on hover. Falls back to `backgroundColor` when not provided.               |
| `transition`  | `string`                                          | smooth base  | CSS `transition` override for the button (e.g. `"all 200ms ease-out"`).                             |
| `opacity`     | `string`                                          | `1`          | CSS `opacity` override for the button (e.g. `"0.5"`).                                               |
| `filter`      | `string`                                          | `none`       | CSS `filter` override (e.g. `"brightness(0.8)"`, `"blur(2px)"`).                                    |
| `floatOnHover`| `boolean`                                         | `false`      | When `true`, applies a subtle float effect on hover (translateY(-2px) + box-shadow) across all variations. |
| `fontSize`    | `string`                                          | variant/size | Font size override (e.g. `"14px"`, `"0.875rem"`).                                                    |
| `fontStyleKey`| `ButtonFontStyleKey`                              | `"avalonBold"` | Key from `fontStyles` controlling font family/weight/style (e.g. `"avalonBold"`, `"fontRegular"`). |
| `fontColor`   | `string`                                          | variant text | Text color override.                                                                                 |
| `flat`        | `boolean`                                         | `false`      | Legacy “flat” appearance for specific variants (e.g. `red`, `white`, `whiteNegative`).               |
| `bordered`    | `boolean`                                         | `false`      | Legacy bordered appearance for specific variants (e.g. `red`).                                       |
| `isActive`    | `boolean`                                         | `false`      | Forces the button into an “active/pressed” visual state.                                             |
| `isSubmitting`| `boolean`                                         | `false`      | Shows a submitting state and disables interaction (sets `aria-busy`).                               |
| `disabled`    | `boolean`                                         | `false`      | Disables the button.                                                                                 |
| `type`        | `"button" \| "submit" \| "reset"`                 | `"button"`   | Native button `type` attribute (only relevant when rendered as a `<button>`).                       |
| `href`        | `string`                                          | `undefined`  | When set, renders the button as an `<a>` with this `href` (styling stays the same).                 |
| `to`          | `string`                                          | `undefined`  | When used with `linkComponent`, passes the route target through (e.g. to react-router’s `Link`).    |
| `linkComponent`| `ElementType`                                    | `undefined`  | Optional link component to render instead of `<button>/<a>` (e.g. `Link` from `react-router-dom`).  |
| `onClick`     | `(event: MouseEvent<HTMLButtonElement>) => void`  | `undefined`  | Standard click handler (from `ButtonHTMLAttributes`).                                                |
| `…rest`       | All other `ButtonHTMLAttributes<HTMLButtonElement>` (except `type` and native `title`) | – | Forwarded to the underlying element (`<button>`, `<a>`, or your `linkComponent`).                    |

### PopUp

```tsx
import { PopUp } from "@podchaser/component-library";

export function Example () {
  return (
    <PopUp
      title="Helpful information"
      details="Explain a feature or provide extra context."
      mainIcon="i"
      showLink
      articleLink="https://www.podchaser.com/"
      outsideClose
    />
  );
}
```

Typical usage is to pass a small icon (e.g. `"i"` wrapped in a styled circle) as `mainIcon`; clicking it toggles the popup. On mobile you can enable a bottom-sheet-style layout with a “Got It” button.

#### PopUp props (overview)

| Prop                 | Type                              | Default     | Description                                                                                  |
| -------------------- | --------------------------------- | ----------- | -------------------------------------------------------------------------------------------- |
| `title`              | `ReactNode`                       | `undefined` | Optional heading rendered at the top of the popup.                                          |
| `details`            | `ReactNode`                       | `undefined` | Main body content for the popup.                                                            |
| `mainIcon`           | `ReactNode`                       | `undefined` | Trigger element; clicking it opens/closes the popup.                                        |
| `toolTipExtraContent`| `() => ReactNode`                 | `undefined` | Optional extra content rendered next to the trigger icon.                                   |
| `showLink`           | `boolean`                         | `false`     | When `true`, renders a “Learn more” link section.                                           |
| `articleLink`        | `string \| null`                  | `null`      | URL used for the “Learn more” link.                                                         |
| `alternateLinkTitle` | `string \| null`                  | `null`      | Custom label for the inline link; falls back to `title` when omitted.                       |
| `onMobile`           | `boolean`                         | `false`     | When `true`, uses the mobile layout (fixed to bottom with “Got It” button).                 |
| `outsideClose`       | `boolean`                         | `false`     | When `true`, clicking outside the popup will close it.                                      |
| `placement`          | `"bottom-center" \| "top-end"`    | `undefined` | Controls alignment / triangle position relative to the trigger icon.                        |
| `maxWidth`           | `number`                          | `292`       | Maximum width of the popup on desktop (in pixels).                                          |
| `eventHandler`       | `() => void`                      | `undefined` | Called whenever the popup is toggled open/closed (including outside-click close).          |
| `expandIcon`         | `ReactNode`                       | `undefined` | Optional icon rendered inside the mobile “Learn more” link.                                 |
| `additionalContent`  | `() => ReactNode`                 | `undefined` | Optional extra content rendered below the details/link section inside the popup container.  |

### SelectDropdown

```tsx
import { SelectDropdown } from "@podchaser/component-library";

const options = [
  { id: "one", title: "One" },
  { id: "two", title: "Two" }
];

export function Example () {
  return (
    <SelectDropdown
      selectOptions={options}
      selected={null}
      ariaLabel="Choose option"
      handleChange={(option) => {
        // handle selected option
        console.log(option);
      }}
    />
  );
}
```

The dropdown uses `Downshift` under the hood and fully controls focus and keyboard accessibility. You control the current selection via the `selected` prop (matching an `option.id`) and respond to changes with `handleChange`.

#### SelectDropdown props (overview)

| Prop                | Type                                                                 | Default     | Description                                                                                           |
|---------------------|----------------------------------------------------------------------|-------------|-------------------------------------------------------------------------------------------------------|
| `selectOptions`     | `SelectOption[]`                                                     | **required**| Array of options rendered in the list.                                                                |
| `selected`          | `string \| null`                                                     | `null`      | ID of the currently selected option (must match a `selectOptions[id]`).                              |
| `ariaLabel`         | `string`                                                             | `"Choose Option"` | Accessible label used for the trigger and placeholder text.                                     |
| `activeStyles`      | `boolean`                                                            | `false`     | When `true`, applies the “selected” visual styling to the trigger and side button.                   |
| `handleChange`      | `(selected: SelectOption \| null) => void`                           | **required**| Called when the user selects an option (or clears selection).                                       |
| `renderOptionIcon`  | `({ option, className }) => ReactNode`                               | `undefined` | Optional render function for an icon next to each option label.                                      |
| `renderToggleIcon`  | `({ isOpen, className }) => ReactNode`                               | `undefined` | Optional render function for the dropdown toggle icon (e.g. chevron).                                |
| `additionalController` | `({ closeMenu, eventIsFieldTrigger, isOpen }) => ReactNode`       | `undefined` | Optional render function for extra controls in the side button area (e.g. “Clear” button).           |
| `children`          | `() => ReactNode`                                                    | `undefined` | Optional render function for extra content rendered below the trigger (helper text, labels, etc.).   |

⸻

Getting started (local development)

# install
npm ci

# run storybook (component dev playground)
npm run storybook

# type-check & build library
npm run build

# test
npm test

# lint (check)
npm run lint

# lint (auto-fix)
npm run lint:fix

# watch build (rollup)
npm run dev


⸻

Scripts

Script	What it does
build	Builds ESM/CJS bundles + types via Rollup
dev	Rollup in watch mode
storybook	Launches Storybook locally on port 6006
build-storybook	Builds static Storybook site
test	Runs Jest (jsdom)
lint	ESLint for src/**/*.ts,tsx
lint:fix	ESLint with --fix
semantic-release	Runs semantic-release (used by CI)


⸻

Tech stack
	•	TypeScript (types, .d.ts output)
	•	Rollup (CJS + ESM)
	•	styled-components (theming, styles)
	•	Storybook (documentation & manual verification)
	•	Jest + @testing-library/react (unit tests)
	•	ESLint (TS rules + React plugin)
	•	semantic-release (automated releases from conventional commits)

⸻

Consuming apps — bundling notes
	•	ESM first: module field → tree-shaking.
	•	CJS fallback: main field for older build pipelines.
	•	Types: types field provides TypeScript declarations.
	•	The library marks React as a peer dependency to avoid duplicate React instances.

⸻

Versioning & releases

This repo is set up for semantic-release. Releases are cut from CI on main (or your configured branch) based on commit messages:
	•	feat: ... → minor
	•	fix: ... → patch
	•	feat!: ... or BREAKING CHANGE: in body → major

Required CI secrets:
	•	GITHUB_TOKEN (automatically provided in GitHub Actions)
	•	NPM_TOKEN (publishing to npm; only if/when you’re ready to publish)

This package currently carries version 0.0.0-development and publishConfig.access = public. Adjust when publishing for real.

⸻

System design (initiative summary)
	•	Purpose: consistent UI via shared components; reduce duplication; speed up dev.
	•	Architecture: NPM library consumed by Podchaser Web; Storybook for docs; tokens via theme provider.
	•	Error handling: typed props, sensible fallbacks, console warnings in dev.
	•	Testing: unit tests for rendering, props, states; optional visual checks via Storybook.
	•	Metrics: adoption rate in app, QA sign-off, developer feedback.
	•	Rollout:
	1.	Audit legacy components/styles
	2.	Build core primitives (Button, Input, Select, etc.)
	3.	Document in Storybook
	4.	Integrate into Podchaser Web
	5.	Replace legacy pieces incrementally
	6.	Validate, then expand

⸻

Contributing
	1.	Create a feature branch:

git switch -c feat/button-variants


	2.	Write code + tests + stories.
	3.	Lint & test locally:

npm run lint
npm test


	4.	Commit using Conventional Commits:
	•	feat(button): add destructive variant
	•	fix(dropdown): close on route change
	5.	Open a PR to main.

Commit hooks via Husky/commitlint can be enabled to enforce commit message format (the dev dependencies are present).

⸻

Project structure (typical)

/src
  /components
    /Button
      index.tsx
	  styled.ts
      Button.stories.tsx
      Button.test.tsx
    /PopUp
      index.tsx
      styled.ts
      PopUp.stories.tsx
      PopUp.test.tsx
    /SelectDropdown
      index.tsx
      styled.ts
      SelectDropdown.stories.tsx
      SelectDropdown.test.tsx
  index.ts            # library public API

rollup.config.mjs
tsconfig.json
jest.config.(ts)


⸻

FAQ

Q: Can I tree-shake unused components?
Yes—consume the ESM build (module field) and import components directly or from the root if it re-exports per-file.

Q: Do I need styled-components in my app?
Only if you use theme-aware components. If a component relies on the theme, wrap your app with ThemeProvider.

Q: Where do I see components live?
Run Storybook locally with npm run storybook.

⸻

Links
	•	Repo: https://github.com/acast-tech/podchaser-ui-library
	•	Issues: https://github.com/acast-tech/podchaser-ui-library/issues

⸻

License

MIT © Darryl Brown