diff --git a/src/app/(app)/dev/ui/page.tsx b/src/app/(app)/dev/ui/page.tsx deleted file mode 100644 index 25c2f3c..0000000 --- a/src/app/(app)/dev/ui/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { UiHarness } from '@/components/dev/UiHarness'; - -export default function UiDevPage() { - return ; -} diff --git a/src/app/(app)/epub/[id]/page.tsx b/src/app/(app)/epub/[id]/page.tsx index b2df05d..4a25c2e 100644 --- a/src/app/(app)/epub/[id]/page.tsx +++ b/src/app/(app)/epub/[id]/page.tsx @@ -1,7 +1,6 @@ 'use client'; import { useParams, useRouter } from "next/navigation"; -import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { EPUBViewer } from '@/components/views/EPUBViewer'; @@ -20,7 +19,7 @@ import { RateLimitBanner } from '@/components/auth/RateLimitBanner'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { useUnmountCleanupRef } from '@/hooks/useUnmountCleanupRef'; -import { buttonClass } from '@/components/ui'; +import { ButtonLink } from '@/components/ui'; import { useEpubDocument } from './useEpubDocument'; export default function EPUBPage() { @@ -172,15 +171,12 @@ export default function EPUBPage() { return (

{error}

- + Back to Documents - +
); } @@ -189,16 +185,12 @@ export default function EPUBPage() { <>
+ Documents - + } title={isLoading ? 'Loading…' : (currDocName || '')} right={ diff --git a/src/app/(app)/html/[id]/page.tsx b/src/app/(app)/html/[id]/page.tsx index d5ec371..f0193a3 100644 --- a/src/app/(app)/html/[id]/page.tsx +++ b/src/app/(app)/html/[id]/page.tsx @@ -1,7 +1,6 @@ 'use client'; import { useParams, useRouter } from "next/navigation"; -import Link from 'next/link'; import { useCallback, useEffect, useRef, useState } from 'react'; import { DocumentSkeleton } from '@/components/documents/DocumentSkeleton'; import { HTMLViewer } from '@/components/views/HTMLViewer'; @@ -18,7 +17,7 @@ import { AudiobookExportModal } from '@/components/AudiobookExportModal'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { useUnmountCleanupRef } from '@/hooks/useUnmountCleanupRef'; -import { buttonClass } from '@/components/ui'; +import { ButtonLink } from '@/components/ui'; import type { TTSAudiobookChapter } from '@/types/tts'; import type { AudiobookGenerationSettings } from '@/types/client'; import { useHtmlDocument } from './useHtmlDocument'; @@ -159,15 +158,12 @@ export default function HTMLPage() { return (

{error}

- + Back to Documents - +
); } @@ -176,16 +172,12 @@ export default function HTMLPage() { <>
+ Documents - + } title={isLoading ? 'Loading…' : (currDocName || '')} right={ diff --git a/src/app/(app)/pdf/[id]/page.tsx b/src/app/(app)/pdf/[id]/page.tsx index bd9f17b..0d1b3b9 100644 --- a/src/app/(app)/pdf/[id]/page.tsx +++ b/src/app/(app)/pdf/[id]/page.tsx @@ -2,7 +2,6 @@ import dynamic from 'next/dynamic'; import { useParams, useRouter } from 'next/navigation'; -import Link from 'next/link'; import { useCallback, useEffect, useRef, useState, type MouseEvent } from 'react'; import { useTTS } from '@/contexts/TTSContext'; import { DocumentSettings } from '@/components/documents/DocumentSettings'; @@ -20,7 +19,7 @@ import { RateLimitBanner } from '@/components/auth/RateLimitBanner'; import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext'; import { useFeatureFlag } from '@/contexts/RuntimeConfigContext'; import { LoadingSpinner } from '@/components/Spinner'; -import { buttonClass } from '@/components/ui'; +import { Button, ButtonLink } from '@/components/ui'; import { FORCE_REPARSE_CONFIRM_MESSAGE, FORCE_REPARSE_CONFIRM_TEXT, @@ -253,16 +252,12 @@ export default function PDFViewerPage() { return (

{error}

- + Back to Documents - +
); } @@ -343,13 +338,14 @@ export default function PDFViewerPage() { {!isLoading && parseUiState === 'failed' ? (
- +
) : null} @@ -382,17 +378,12 @@ export default function PDFViewerPage() { <>
+ Documents - + } title={isLoading ? 'Loading…' : (currDocName || '')} right={ diff --git a/src/app/(public)/layout.tsx b/src/app/(public)/layout.tsx index 4b08f30..f850899 100644 --- a/src/app/(public)/layout.tsx +++ b/src/app/(public)/layout.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; import Link from 'next/link'; import { getResolvedRuntimeConfigForRsc } from '@/lib/server/runtime-config-rsc'; -import { buttonClass } from '@/components/ui'; +import { ButtonAnchor, ButtonLink } from '@/components/ui'; import './public.css'; export default async function PublicLayout({ children }: { children: ReactNode }) { @@ -42,12 +42,8 @@ export default async function PublicLayout({ children }: { children: ReactNode } GitHub
@@ -70,18 +66,11 @@ export default async function PublicLayout({ children }: { children: ReactNode }

{enableUserSignups ? ( - - Create account - + Create account ) : null} - + Star on GitHub - +
diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 49ead03..069ca95 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -2,7 +2,7 @@ import type { CSSProperties } from 'react'; import type { Metadata } from 'next'; import Link from 'next/link'; import { getResolvedRuntimeConfigForRsc } from '@/lib/server/runtime-config-rsc'; -import { buttonClass } from '@/components/ui'; +import { ButtonAnchor, ButtonLink } from '@/components/ui'; export const metadata: Metadata = { title: 'Open Source Read-Along Workspace', @@ -98,24 +98,13 @@ export default async function LandingPage() {

- - Open the reader - + Open the reader {enableUserSignups ? ( - - Create account - + Create account ) : ( - - Sign in - + Sign in )} - - Read the docs → - + Read the docs →
@@ -316,22 +305,12 @@ export default async function LandingPage() { external compute worker. Every piece is yours to host.

- + View the repository - - + + Deployment guides - +
diff --git a/src/app/(public)/privacy/page.tsx b/src/app/(public)/privacy/page.tsx index 30818b5..96ac56b 100644 --- a/src/app/(public)/privacy/page.tsx +++ b/src/app/(public)/privacy/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next'; import Link from 'next/link'; import { headers } from 'next/headers'; -import { buttonClass } from '@/components/ui'; +import { ButtonAnchor, ButtonLink } from '@/components/ui'; export const metadata: Metadata = { title: 'Privacy & Data Usage', @@ -181,25 +181,13 @@ export default async function PrivacyPage() { want full infrastructure control.

- + Open an Issue - - + + Self-Hosting Guide - - - Back to landing - + + Back to landing
diff --git a/src/components/AudiobookExportModal.tsx b/src/components/AudiobookExportModal.tsx index ca378e7..1f9e7e3 100644 --- a/src/components/AudiobookExportModal.tsx +++ b/src/components/AudiobookExportModal.tsx @@ -1,7 +1,7 @@ 'use client'; import { Fragment, useState, useRef, useCallback, useEffect, useMemo } from 'react'; -import { Transition, Listbox, Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/react'; +import { Transition, Listbox, Menu, MenuButton } from '@headlessui/react'; import { useTimeEstimation } from '@/hooks/useTimeEstimation'; import { ProgressPopup } from '@/components/ProgressPopup'; import { ProgressCard } from '@/components/ProgressCard'; @@ -13,7 +13,7 @@ import { VoicesControlBase } from '@/components/player/VoicesControlBase'; import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell'; import { resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy'; import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts'; -import { Button, Card, IconButton, MenuItemClass, SharedListboxButton, SharedListboxOption, SharedListboxOptions, cn, menuPanelClass, rangeInputClass } from '@/components/ui'; +import { Button, Card, IconButton, MenuActionItem, MenuItemsSurface, RangeInput, SharedListboxButton, SharedListboxOption, SharedListboxOptions, cn } from '@/components/ui'; import { getAudiobookStatus, deleteAudiobookChapter, @@ -621,14 +621,12 @@ export function AudiobookExportModal({ {formatSpeed(nativeSpeed)}x - setNativeSpeed(parseFloat(e.target.value))} - className={rangeInputClass} />
0.5x @@ -645,14 +643,12 @@ export function AudiobookExportModal({ {formatSpeed(postSpeed)}x
- setPostSpeed(parseFloat(e.target.value))} - className={rangeInputClass} />
0.5x @@ -794,68 +790,48 @@ export function AudiobookExportModal({ leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - {chapter.status === 'completed' && ( <> - - {({ active }) => ( - - )} - - - {({ active }) => ( - - )} - + setPendingDeleteChapter(chapter)} + title="Delete this chapter" + > + + Delete + + handleDownloadChapter(chapter)}> + + Download + )} {regeneratingChapter === chapter.index && ( - - {({ active }) => ( - - )} - + + + Cancel + )} {onRegenerateChapter && !isGenerating && ( - - {({ active, disabled }) => ( - - )} - + handleRegenerateChapter(chapter)} + title="Regenerate this chapter" + > + + {regeneratingChapter === chapter.index ? 'Regenerating...' : 'Regenerate'} + )} - + {/* end of menu items */} diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx index f2d327c..3217389 100644 --- a/src/components/CodeBlock.tsx +++ b/src/components/CodeBlock.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { CopyIcon, CheckIcon } from '@/components/icons/Icons'; -import { buttonClass } from '@/components/ui'; +import { Button } from '@/components/ui'; export function CodeBlock({ children }: { children: string }) { const [copied, setCopied] = useState(false); @@ -19,17 +19,15 @@ export function CodeBlock({ children }: { children: string }) { font-mono text-foreground border border-line"> {children} - +
); } diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index e203595..374ec34 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -1,10 +1,10 @@ 'use client'; import { useRef, useState, useEffect, useCallback } from 'react'; -import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'; +import { Popover, PopoverButton } from '@headlessui/react'; import { isLightColor, type CustomThemeColors } from '@/contexts/ThemeContext'; import { PaletteIcon } from '@/components/icons/Icons'; -import { IconButton, Input, cn, popoverPanelClass } from '@/components/ui'; +import { IconButton, Input, PopoverSurface } from '@/components/ui'; /** * Curated swatch palettes per color role, sourced from existing themes @@ -89,13 +89,10 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps) /> - {/* Label */}
@@ -173,7 +170,7 @@ export function ColorPicker({ value, field, label, onChange }: ColorPickerProps) className="flex-1 font-mono" />
-
+ ); } diff --git a/src/components/CookieConsentBanner.tsx b/src/components/CookieConsentBanner.tsx index 3e1a278..de748e0 100644 --- a/src/components/CookieConsentBanner.tsx +++ b/src/components/CookieConsentBanner.tsx @@ -4,7 +4,7 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { getConsentState, setConsentState } from '@/lib/client/analytics'; import { Transition } from '@headlessui/react'; -import { buttonClass } from '@/components/ui'; +import { Button } from '@/components/ui'; export function CookieConsentBanner() { const [show, setShow] = useState(false); @@ -55,26 +55,22 @@ export function CookieConsentBanner() {
- - +
diff --git a/src/components/ProgressCard.tsx b/src/components/ProgressCard.tsx index 380a45a..f8341d0 100644 --- a/src/components/ProgressCard.tsx +++ b/src/components/ProgressCard.tsx @@ -1,4 +1,4 @@ -import { buttonClass } from '@/components/ui'; +import { Button } from '@/components/ui'; interface ProgressCardProps { progress: number; @@ -52,17 +52,15 @@ export function ProgressCard({ )} - + {/* Progress bar */} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 7d71964..3114e6b 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -4,9 +4,6 @@ import { Fragment, useState, useEffect, useCallback, useMemo } from 'react'; import { Transition, Listbox, - ListboxButton, - ListboxOptions, - ListboxOption, } from '@headlessui/react'; import Link from 'next/link'; import { useTheme } from '@/contexts/ThemeContext'; @@ -56,9 +53,9 @@ import { ModalFrame, ModalTitle, inputClass, - listboxButtonClass, - listboxOptionClass, - listboxOptionsClass, + SharedListboxButton, + SharedListboxOption, + SharedListboxOptions, } from '@/components/ui'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -692,28 +689,26 @@ export function SettingsModal({ setCustomModelInput(''); }} > - + {selectedProviderOption?.name || 'Select Provider'} - + - {ttsProviders.map((provider) => ( - listboxOptionClass(active)} value={provider} > {({ selected }) => ( @@ -728,9 +723,9 @@ export function SettingsModal({ )} )} - + ))} - + )} @@ -797,7 +792,7 @@ export function SettingsModal({ } }} > - + {selectedModel ? ( @@ -815,21 +810,19 @@ export function SettingsModal({ - + - {ttsModels.map((model) => ( - listboxOptionClass(active)} value={model} > {({ selected }) => ( @@ -849,9 +842,9 @@ export function SettingsModal({ )} )} - + ))} - + diff --git a/src/components/admin/AdminFeaturesPanel.tsx b/src/components/admin/AdminFeaturesPanel.tsx index af55edc..162e4a5 100644 --- a/src/components/admin/AdminFeaturesPanel.tsx +++ b/src/components/admin/AdminFeaturesPanel.tsx @@ -1,7 +1,7 @@ 'use client'; import { Fragment, useEffect, useMemo, useState } from 'react'; -import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react'; +import { Listbox, Transition } from '@headlessui/react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { ChevronUpDownIcon, CheckIcon } from '@/components/icons/Icons'; @@ -10,9 +10,9 @@ import { Section, ToggleRow, inputClass, - listboxButtonClass, - listboxOptionClass, - listboxOptionsClass, + SharedListboxButton, + SharedListboxOption, + SharedListboxOptions, Button, } from '@/components/ui'; import { type TtsProviderId } from '@/lib/shared/tts-provider-catalog'; @@ -205,24 +205,23 @@ export function AdminFeaturesPanel() { {providerOptions.length > 0 ? ( - + {selectedProviderOption?.name ?? 'Select provider'} - + - + {providerOptions.map((opt) => ( - listboxOptionClass(active)} > {({ selected }) => ( <> @@ -236,9 +235,9 @@ export function AdminFeaturesPanel() { )} )} - + ))} - + ) : ( diff --git a/src/components/admin/AdminProvidersPanel.tsx b/src/components/admin/AdminProvidersPanel.tsx index 2f61ba3..8960fe9 100644 --- a/src/components/admin/AdminProvidersPanel.tsx +++ b/src/components/admin/AdminProvidersPanel.tsx @@ -1,7 +1,7 @@ 'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'; +import { Listbox, Menu, MenuButton, Transition } from '@headlessui/react'; import { Fragment } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; @@ -14,15 +14,14 @@ import { Section, ToggleRow, inputClass, - listboxButtonClass, - listboxOptionClass, - listboxOptionsClass, + SharedListboxButton, + SharedListboxOption, + SharedListboxOptions, Button, IconButton, Input, - MenuItemClass, - cn, - menuPanelClass, + MenuItemsSurface, + MenuActionItem, } from '@/components/ui'; type ProviderType = TtsProviderId; @@ -426,24 +425,23 @@ export function AdminProvidersPanel() { setCustomModelInput(''); }} > - + {selectedProviderType.label} - + - + {PROVIDER_TYPE_OPTIONS.map((opt) => ( - listboxOptionClass(active)} > {({ selected }) => ( <> @@ -457,9 +455,9 @@ export function AdminProvidersPanel() { )} )} - + ))} - + @@ -485,26 +483,25 @@ export function AdminProvidersPanel() { setCustomModelInput(''); }} > - + {selectedModelDefinition?.name ?? 'Select model'} - + - + {modelDefinitions.map((model) => ( - listboxOptionClass(active)} > {({ selected }) => ( <> @@ -523,9 +520,9 @@ export function AdminProvidersPanel() { )} )} - + ))} - + {supportsCustomModel && selectedModelId === 'custom' && ( @@ -663,56 +660,26 @@ export function AdminProvidersPanel() { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - - - {({ active }) => ( - - )} - - - {({ active, disabled }) => ( - - )} - - - {({ active }) => ( - - )} - - - {({ active }) => ( - - )} - - + startEdit(p)}> + Edit + + setDefault(p.slug)} + disabled={!p.enabled || defaultProviderSlug === p.slug} + > + Set as default + + toggleEnabled(p)}> + {p.enabled ? 'Disable' : 'Enable'} + + remove(p.id)}> + Delete + + diff --git a/src/components/auth/UserMenu.tsx b/src/components/auth/UserMenu.tsx index c459eb1..e780d2a 100644 --- a/src/components/auth/UserMenu.tsx +++ b/src/components/auth/UserMenu.tsx @@ -7,7 +7,7 @@ import { useAuthSession } from '@/hooks/useAuthSession'; import { getAuthClient } from '@/lib/client/auth-client'; import { useRouter } from 'next/navigation'; import { UserIcon } from '@/components/icons/Icons'; -import { IconButton, SidebarNavItem, SidebarNavLink, buttonClass } from '@/components/ui'; +import { ButtonLink, IconButton, SidebarNavItem, SidebarNavLink } from '@/components/ui'; type UserMenuVariant = 'toolbar' | 'sidebar'; @@ -57,13 +57,13 @@ export function UserMenu({ return (
- + Connect - + {enableUserSignups && ( - + Create account - + )}
); diff --git a/src/components/dev/UiHarness.tsx b/src/components/dev/UiHarness.tsx deleted file mode 100644 index 11b4764..0000000 --- a/src/components/dev/UiHarness.tsx +++ /dev/null @@ -1,188 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { THEMES, useTheme } from '@/contexts/ThemeContext'; -import { - Badge, - Button, - Card, - CheckItem, - Divider, - Field, - IconButton, - Input, - Panel, - Section, - Select, - Sidebar, - Surface, - Switch, - Textarea, - ToggleRow, - Toolbar, - ToolbarButton, - ToolbarGroup, - ToolbarSegment, - buttonClass, - dropzoneSurfaceClass, - popoverPanelClass, - rangeInputClass, -} from '@/components/ui'; - -const THEME_OPTIONS = THEMES.filter((theme) => theme !== 'custom'); - -export function UiHarness() { - const { theme, setTheme } = useTheme(); - const [enabled, setEnabled] = useState(true); - const [checked, setChecked] = useState(false); - const [selectValue, setSelectValue] = useState('library'); - - return ( -
-
-
-
- Authenticated dev surface -

UI system harness

-

- Review shared primitives, state tokens, and theme behavior from the authenticated app shell. -

-
-
- - -
-
- -
-
- {THEME_OPTIONS.map((option) => ( - - ))} -
-
- -
- -
- - - - - - - - × -
- - - -
- - - - -