Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BandScope

BandScope is a public GitHub project for a local-first desktop app that turns a song into a practical rehearsal view: likely harmony by section and by instrument or vocal role, section roadmap, tempo and groove cues, separated stems, playable ranges, simplification hints, transposition or capo guidance, overlap cues, visible confidence, and rehearsal priorities without DAW complexity.
BandScope is a public GitHub project for a local-first desktop app that turns a song into a practical rehearsal view: likely harmony by section and by instrument or vocal role, section roadmap, tempo and groove cues, rough stem previews, playable ranges, simplification hints, transposition or capo guidance, overlap cues, visible confidence, and rehearsal priorities without DAW complexity.

It does not promise notation-grade full arrangement transcription or DAW-style production editing.

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
storybook-static
23 changes: 23 additions & 0 deletions apps/desktop/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { StorybookConfig } from "@storybook/react-vite"
import tailwindcss from "@tailwindcss/vite"
import path from "node:path"
import { fileURLToPath } from "node:url"

const dir = path.dirname(fileURLToPath(import.meta.url))

const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(ts|tsx)"],
addons: [],
framework: { name: "@storybook/react-vite", options: {} },
viteFinal: async (viteConfig) => {
viteConfig.plugins = [...(viteConfig.plugins ?? []), tailwindcss()]
viteConfig.resolve = viteConfig.resolve ?? {}
viteConfig.resolve.alias = {
...(viteConfig.resolve.alias ?? {}),
"@": path.resolve(dir, "../src"),
}
return viteConfig
},
}

export default config
14 changes: 14 additions & 0 deletions apps/desktop/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Preview } from "@storybook/react-vite"

import "../src/index.css"

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

export default preview
5 changes: 5 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"lint": "eslint \"src/**/*.{ts,tsx}\" vite.config.ts",
"typecheck": "tsc --noEmit",
"test": "node -e \"require('node:fs').mkdirSync('coverage/.tmp', { recursive: true })\" && vitest run --coverage"
Expand All @@ -20,10 +22,12 @@
"lucide-react": "^1.20.0",
"react": "^19.2.4",
"react-dom": "^19.2.7",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@storybook/react-vite": "^10.4.6",
"@tailwindcss/vite": "^4.3.1",
"@tauri-apps/cli": "^2.11.2",
"@testing-library/jest-dom": "^6.6.3",
Expand All @@ -35,6 +39,7 @@
"@vitest/coverage-v8": "^4.1.5",
"eslint": "^10.5.0",
"jsdom": "^29.1.1",
"storybook": "^10.4.6",
"tailwindcss": "^4.2.4",
"typescript": "^6.0.3",
"typescript-eslint": "^8.60.1",
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { EmptyState, ErrorState, LoadingState } from "./features/workspace/Works
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { Toaster } from "@/components/ui/sonner";

const ANALYSIS_POLL_INTERVAL_MS = 250;
const MAX_ERROR_DETAIL_LENGTH = 220;
Expand Down Expand Up @@ -736,6 +737,8 @@ export function App() {
</section>
</main>
</div>

<Toaster />
</div>
);
}
76 changes: 76 additions & 0 deletions apps/desktop/src/components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client"

import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
import { ChevronDown } from "lucide-react"

import { cn } from "@/lib/utils"

/** Render a vertically stacked set of collapsible sections. */
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
return (
<AccordionPrimitive.Root
data-slot="accordion"
className={cn("w-full", className)}
{...props}
/>
)
}

/** Render one collapsible accordion item. */
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
)
}

/** Render the clickable header that toggles an item open or closed. */
function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.Trigger.Props) {
return (
<AccordionPrimitive.Header data-slot="accordion-header" className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"flex flex-1 items-center justify-between gap-4 py-4 text-left text-sm font-medium outline-none transition-all",
"hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"disabled:pointer-events-none disabled:opacity-50",
"[&[data-panel-open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
)
}

/** Render the collapsible content panel of an item. */
function AccordionContent({
className,
children,
...props
}: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
className={cn(
"overflow-hidden text-sm data-[ending-style]:h-0 data-[starting-style]:h-0",
className
)}
{...props}
>
<div className="pt-0 pb-4">{children}</div>
</AccordionPrimitive.Panel>
)
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
107 changes: 107 additions & 0 deletions apps/desktop/src/components/ui/breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as React from "react"
import { ChevronRight, MoreHorizontal } from "lucide-react"

import { cn } from "@/lib/utils"

/** Render the breadcrumb navigation landmark. */
function Breadcrumb(props: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}

/** Render the ordered list of breadcrumb items. */
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}

/** Render a single breadcrumb item. */
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}

/** Render a navigable breadcrumb link. */
function BreadcrumbLink({ className, ...props }: React.ComponentProps<"a">) {
return (
<a
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
)
}

/** Render the current page (non-navigable) breadcrumb item. */
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
)
}

/** Render the separator between breadcrumb items. */
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}

/** Render a collapsed-items indicator. */
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}

export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}
22 changes: 22 additions & 0 deletions apps/desktop/src/components/ui/button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from "@storybook/react-vite"

import { Button } from "./button"

const meta = {
title: "UI/Button",
component: Button,
parameters: { layout: "centered" },
args: { children: "분석 시작" },
} satisfies Meta<typeof Button>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {}
export const Outline: Story = {
args: { variant: "outline", children: "로컬 오디오 선택" },
}
export const Destructive: Story = {
args: { variant: "destructive", children: "삭제" },
}
export const Small: Story = { args: { size: "sm" } }
17 changes: 17 additions & 0 deletions apps/desktop/src/components/ui/checkbox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from "@storybook/react-vite"

import { Checkbox } from "./checkbox"

const meta = {
title: "UI/Checkbox",
component: Checkbox,
parameters: { layout: "centered" },
} satisfies Meta<typeof Checkbox>

export default meta
type Story = StoryObj<typeof meta>

export const Unchecked: Story = {}
export const Checked: Story = { args: { defaultChecked: true } }
export const Indeterminate: Story = { args: { indeterminate: true } }
export const Disabled: Story = { args: { disabled: true, defaultChecked: true } }
39 changes: 39 additions & 0 deletions apps/desktop/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client"

import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
import { Check, Minus } from "lucide-react"

import { cn } from "@/lib/utils"

/** Render an accessible checkbox with checked and indeterminate states. */
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer inline-flex items-center justify-center size-4 shrink-0 rounded-[4px] border border-input shadow-xs outline-none transition-shadow",
"data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground",
"data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
"disabled:cursor-not-allowed disabled:opacity-50",
"dark:bg-input/30",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current"
>
{props.indeterminate ? (
<Minus className="size-3.5" />
) : (
<Check className="size-3.5" />
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}

export { Checkbox }
Loading
Loading