Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 52 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Development

This repo uses Bun workspaces. The docs site and registry live in `apps/www`.

## Setup

```bash
bun install
cd apps/www
bun run dev
```

The local site runs on the Next.js dev server.

## Commands

Run these from the repo root unless the command says otherwise.

```bash
bun run lint
bun run format:check
bun run test:local
```

Registry and site checks:

```bash
cd apps/www
bun run registry:build
bun run validate:registries
bun run build
```

## Project Layout

| Path | Purpose |
| --- | --- |
| `apps/www/content/docs` | Fumadocs documentation |
| `apps/www/registry/default/ui` | Copyable UI primitives |
| `apps/www/registry/default/hooks` | Feature hooks and utilities |
| `apps/www/registry/default/blocks` | Complete player blocks |
| `apps/www/registry/collection` | shadcn registry metadata |
| `apps/www/public/r` | Generated registry JSON |
| `prompts` | Project rules for local assistants and review tooling |

## Contribution Notes

- Public examples should import from `@/components/limeplay/*` and `@/hooks/limeplay/*`, not `@/registry/*`.
- New or changed components, hooks, and utilities need registry metadata.
- Registry dependencies should be explicit and major versions with breaking changes should be pinned.
- Blocks should keep public loading props consistent: `source`, `loading`, `sourceKey`, `autoLoad`, `initialIndex`, and `mediaProps`.
- Use feature selectors and `useMediaEvents()` instead of deleted convenience hooks or `on*` callback fields.
50 changes: 43 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,52 @@
# Limeplay - Modern UI Library for Video Players
# Limeplay

<img src="./apps/www/public/opengraph-image.png" alt="Limeplay Banner" />
<img src="./apps/www/public/opengraph-image.png" alt="Limeplay preview" />

🔰 [@shadcn](https://ui.shadcn.com/docs/cli) CLI based UI library for building video player in React. Uses [@shaka-player](https://github.com/shaka-project/shaka-player) as the playback engine. Accessible and customizable components & hooks that you can copy and paste into your apps.
Copy-paste media player components for React. Limeplay follows the shadcn/ui model: install a block, get the files in your app, and customize them like code you wrote yourself.

🏗️ Checkout [Blocks](https://limeplay.winoffrg.dev/blocks) for cooked examples.
Built on Shaka Player for HLS, DASH, adaptive bitrate, and DRM-capable playback.

## Documentation
[Documentation](https://limeplay.winoffrg.dev/docs/quick-start) · [Blocks](https://limeplay.winoffrg.dev/blocks/video-player) · [Discord](https://discord.gg/ZjXFzqmqjn) · [Development](./DEVELOPMENT.md)

Visit https://limeplay.winoffrg.dev/docs/quick-start to view the documentation.
## Install

Discussion and ideas on our [Discord Server](https://discord.gg/ZjXFzqmqjn)
Start with a React project that already has shadcn/ui initialized.

```bash
npx shadcn@latest init
```

Add the video player block:

```bash
npx shadcn add @limeplay/video-player
```

Use it anywhere in your app:

```tsx
import { VideoPlayer } from "@/components/limeplay/video-player/components/media-player"

export function Player() {
return (
<VideoPlayer source="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd" />
)
}
```

> [!TIP]
> You can find more blocks in [Limeplay Blocks](https://limeplay.winoffrg.dev/blocks/video-player) showcase.

<img src="./apps/www/public/github_preview.png" alt="Player teaser" />

## Goals

1. Ship production-grade media players without rebuilding the hard parts. Limeplay gives you Netflix, YouTube, and Spotify-style interaction patterns with real playback behavior, not static UI.
2. Cover the details users notice: resilient error states, accessible controls, keyboard navigation, responsive layouts, browser support, and modern interaction states.
3. Keep ownership of the UI. Limeplay handles player logic, state, events, playlists, and media controls while the components stay fully editable in your app.
4. Build on a serious playback engine. Shaka Player brings HLS, DASH, live streaming, DRM-capable playback, adaptive bitrate, and more.

For custom layouts, feature hooks, and API references, start with the [quick start docs](https://limeplay.winoffrg.dev/docs/quick-start).

## License

Expand Down
8 changes: 5 additions & 3 deletions apps/www/components/block-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,16 @@ function BlockViewerToolbar() {
gap-1
*:data-[slot=toggle-group-item]:size-6! *:data-[slot=toggle-group-item]:rounded-sm!
`}
defaultValue="100"
defaultValue={["100"]}
onValueChange={(value) => {
const nextValue = value[0]
if (!nextValue) return

setView("preview")
if (resizablePanelRef?.current) {
resizablePanelRef.current.resize(parseInt(value))
resizablePanelRef.current.resize(parseInt(nextValue))
}
}}
type="single"
>
<ToggleGroupItem title="Desktop" value="100">
<Monitor className="size-3.5" />
Expand Down
3 changes: 2 additions & 1 deletion apps/www/components/blocks/block-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
CodeXmlIcon,
CogIcon,
type LucideIcon,
Maximize2Icon,
Minimize2Icon,
MoonIcon,
Expand Down Expand Up @@ -228,7 +229,7 @@ export function BlockToolbar({

type ToolbarItem = {
active: boolean
icon: React.ElementType
icon: LucideIcon
iconStyle?: string
id: string
label: string
Expand Down
4 changes: 2 additions & 2 deletions apps/www/components/blocks/preview-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export function BlockPreviewWithToolbar({
)

const handleExpandToggle = useCallback(() => {
setExpanded((currentExpanded) => {
const nextExpanded = !currentExpanded
setExpanded((previousExpanded) => {
const nextExpanded = !previousExpanded
updateExpandedQuery(nextExpanded)

@cubic-dev-ai cubic-dev-ai Bot Jun 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Side effect inside state updater; URL replace can run more than once for a single toggle. Move query-sync side effect outside the updater (e.g., effect driven by expanded).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/www/components/blocks/preview-pane.tsx, line 56:

<comment>Side effect inside state updater; URL replace can run more than once for a single toggle. Move query-sync side effect outside the updater (e.g., effect driven by `expanded`).</comment>

<file context>
@@ -51,10 +51,12 @@ export function BlockPreviewWithToolbar({
-  }, [expanded, updateExpandedQuery])
+    setExpanded((previousExpanded) => {
+      const nextExpanded = !previousExpanded
+      updateExpandedQuery(nextExpanded)
+      return nextExpanded
+    })
</file context>
Fix with cubic

return nextExpanded
})
Expand Down
23 changes: 17 additions & 6 deletions apps/www/components/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,28 @@ import type { ReactNode } from "react"
import { createGenerator } from "fumadocs-typescript"
import { AutoTypeTable } from "fumadocs-typescript/ui"
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock"
import * as TabsComponents from "fumadocs-ui/components/tabs"
import {
Tab,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "fumadocs-ui/components/tabs"
import defaultComponents from "fumadocs-ui/mdx"

import { ComponentPreview } from "@/components/component-preview"
import { Mermaid } from "@/components/mdx/mermaid"

const generator = createGenerator()
const fumadocsComponents = defaultComponents as MDXComponents

export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultComponents,
AutoTypeTable: (props) => (
...fumadocsComponents,
Attribution,
AutoTypeTable: (props: React.ComponentProps<typeof AutoTypeTable>) => (
<AutoTypeTable {...props} generator={generator} />
),
...TabsComponents,
Attribution,
ComponentPreview,
License,
Mermaid,
Expand All @@ -28,8 +34,13 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
<Pre>{props.children}</Pre>
</CodeBlock>
),
Tab,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
...components,
}
} as unknown as MDXComponents
}

function Attribution({
Expand Down
48 changes: 46 additions & 2 deletions apps/www/components/players/audio-player/hover-player.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"use client"

import { IconVoiceHigh } from "@central-icons-react/round-filled-radius-0-stroke-1/IconVoiceHigh"
import { ListMusicIcon, SquareArrowOutUpRightIcon } from "lucide-react"
import { motion } from "motion/react"
import Link from "next/link"
import { useCallback, useEffect, useState } from "react"

import {
StreamPanel,
Expand All @@ -12,7 +12,9 @@ import {
} from "@/components/stream-panel"
import { useStreamPanelSync } from "@/components/stream-panel/use-stream-panel-sync"
import { Button } from "@/components/ui/button"
import { type AgentState, Orb } from "@/components/ui/orb"
import { PopoverTrigger } from "@/components/ui/popover"
import { usePlaybackStore } from "@/registry/default/hooks/use-playback"

import { AudioPlayerDemo } from "./demo-player"

Expand All @@ -24,6 +26,30 @@ export function AudioPlayerHover() {
)
}

function AudioHoverOrbSync({
onAgentStateChange,
}: {
onAgentStateChange: (agentState: AgentState) => void
}) {
const status = usePlaybackStore((state) => state.status)

useEffect(() => {
if (status === "playing") {
onAgentStateChange("talking")
return
}

if (status === "buffering" || status === "loading") {
onAgentStateChange("thinking")
return
}

onAgentStateChange(null)
}, [onAgentStateChange, status])

return null
}

function AudioHoverStreamPanel() {
const { handleLoadStream, handlePlaylistPresetChange, handlePresetChange } =
useStreamPanelSync({ playerType: "audio" })
Expand Down Expand Up @@ -56,6 +82,13 @@ function AudioHoverStreamTrigger() {

function AudioPlayerHoverContent() {
const { open } = useStreamPanel()
const [orbAgentState, setOrbAgentState] = useState<AgentState>(null)
const isAudioActive =
orbAgentState === "talking" || orbAgentState === "thinking"

const handleAgentStateChange = useCallback((agentState: AgentState) => {
setOrbAgentState(agentState)
}, [])

return (
<motion.div
Expand All @@ -80,7 +113,17 @@ function AudioPlayerHoverContent() {
>
<div className="flex h-14 w-fit flex-row items-center gap-3 rounded-t-3xl bg-black px-4">
<div className="flex w-full items-center justify-center gap-1">
<IconVoiceHigh className="size-8 text-white" />
<span aria-hidden="true" className="relative size-9 shrink-0">
<Orb
agentState={orbAgentState}
className="relative size-full"
colors={["#ED0040", "#F42A8B"]}
manualInput={isAudioActive ? 0.55 : 0}
manualOutput={isAudioActive ? 0.85 : 0}
seed={1528}
volumeMode="manual"
/>
</span>
<span className="text-3xl font-semibold tracking-tight text-white">
Audio
</span>
Expand All @@ -99,6 +142,7 @@ function AudioPlayerHoverContent() {
</div>
</motion.div>
<AudioPlayerDemo>
<AudioHoverOrbSync onAgentStateChange={handleAgentStateChange} />
<AudioHoverStreamPanel />
</AudioPlayerDemo>
</motion.div>
Expand Down
Loading
Loading