feat(ui): migrate to daisyUI 5 and Tailwind CSS 4 (#5884)

* feat(ui): migrate to daisyUI 5 and Tailwind CSS 4

daisyUI 5 requires Tailwind CSS 4, so the configuration moves into
globals.css: tailwind.config.ts is gone, and the built-in themes now
compile through a small plugin (src/styles/daisyui-themes.ts) that
shares one token emitter with the runtime custom theme injector.

Keep the rendering identical to daisyUI 4:

- themes.ts emits daisyUI 5 --color-* tokens, derives the *-content
  colors with the v4 formula, and pins the v4 radii, border and depth.
- globals.css carries a compat block for the component defaults v5
  changed: md field height, small size type, checkbox radius, menu
  width and line height, disabled non-buttons, toggle width, and the
  visibility of closed and nested dropdowns. Every rule has zero
  specificity so utilities keep winning as they did on v3.
- Pin the Tailwind 3 color palette for the families the app uses;
  Tailwind 4 ships oklch values that render noticeably more vivid.
- Restore the v3 preflight defaults the UI was tuned on (border and
  placeholder colors, button cursor) and the v3 hover semantics.

Class names are migrated for both upgrades: the v3 renames (shadow,
rounded, blur, outline-none, flex-shrink, the important modifier, CSS
variables in arbitrary values) and the daisyUI removals
(input-bordered, form-control, label-text, tr.hover). Three call sites
relied on the old stylesheet order of conflicting utilities and now
say what they mean instead.

Fixes #5587: daisyUI 5 opts the select into appearance: base-select and
themes ::picker(select), so the option popup follows the app theme
instead of the operating system. The chromeless settings selects no
longer force appearance-none, which would cancel that.

The font dropdown's System Fonts sub-list now opens only when its row
is activated, and its bottom edge lines up with the parent menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(nix): refresh pnpmDeps hash for the daisyUI 5 lockfile

The daisyUI and Tailwind upgrade changes pnpm-lock.yaml, so the pinned
fixed-output hash no longer matches. Value taken from the got: line of
the nix-deps-check run on this pull request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): keep the daisyUI toggle size modifiers

The compat rule that restored the v4 track width applied to every
toggle, so toggle-sm and toggle-xs were stretched to the medium width:
the OPDS catalog manager's auto-download switches came out 48px wide on
a 20px track. daisyUI 4 sized each step separately and the steps do not
share one ratio, so pin all four.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(android): require WebView 111 and use btn-contrast for catalog save

Tailwind CSS 4 targets Chromium 111 and up, and the oklch fallback that
used to cover older WebViews is gone with the daisyUI 5 migration, so
raise the webview-upgrade plugin's minSupportedMajor from 92 to 111.
Below that the plugin now tells the user their WebView is too old
instead of letting the app render with broken colors.

Also switch the OPDS catalog dialog's submit button to btn-contrast,
which is the design system's primary action for dialogs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(ios): require iOS 16.4

Tailwind CSS 4 targets Safari 16.4 and up. Below that, color-mix() and
@property are missing, so daisyUI's controls do not degrade gracefully:
input borders fall back to currentColor and the toggle geometry
collapses, as seen on an iOS 15.8 device. Raising the deployment target
keeps those devices on the last build that renders correctly instead of
shipping them a broken UI.

Covers the Tauri bundle config, the xcodegen project and every target in
the generated Xcode project.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(deps): point the webview-upgrade plugin at the 111 default

Picks up readest/tauri-plugin-webview-upgrade#2, which raises the
plugin's own defaultMinSupportedMajor to match the minSupportedMajor
this app already sets in tauri.conf.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin 2026-08-26 22:09:41 +08:00 • committed by GitHub
parent 2fd8b3bc92
commit 8ef527af70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
185 changed files with 2746 additions and 2201 deletions

View file

@ -276,7 +276,7 @@ className="btn btn-ghost"
className={clsx(
'rounded-lg px-4 py-2 text-sm font-medium',
'hover:bg-base-200 transition-colors duration-150',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-base-content/15',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-base-content/15',
)}
```
@ -331,7 +331,7 @@ Anatomy:
'transition-colors duration-150',
'hover:border-base-300 hover:bg-base-200/60',
'active:bg-base-200/80',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<span
@ -530,14 +530,16 @@ When a control sits inside a bordered card, it shouldn't carry its own
border or fill. The card supplies the visual boundary; the control just
sits on the row.
- **Selects:** drop `select-bordered` and `eink-bordered`. Add
`!bg-transparent !bg-none !appearance-none` to suppress daisyui's
background chevron and native arrow. Render a real `<MdArrowDropDown>`
- **Selects:** drop `eink-bordered`. Add
`bg-transparent! bg-none!` to suppress daisyui's background chevron (its
`.select` already hides the native arrow; never add `appearance-none!`, that
would cancel the `appearance: base-select` popup that keeps the options
themed instead of OS-drawn, #5587). Render a real `<MdArrowDropDown>`
icon at the cell's trailing edge for the affordance — see "End-aligned
values" below.
- **Inputs:** drop `input-bordered` and `eink-bordered`. Add `!bg-transparent`
with `hover:!bg-base-200/60 focus:!bg-base-200/60` so the field still
signals interactability. Use `text-end` and `!pe-0` so the value sits
- **Inputs:** drop `eink-bordered`. Add `bg-transparent!`
with `hover:bg-base-200/60! focus:bg-base-200/60!` so the field still
signals interactability. Use `text-end` and `pe-0!` so the value sits
flush against the row's trailing edge.
- **Toggles:** untouched — they're already chromeless.
@ -564,18 +566,18 @@ with the card's own border and double-stack with adjacent rows.
```tsx
<div className='hover:bg-base-200/60 focus-within:bg-base-200/60 flex max-w-[60%] items-center rounded-md'>
<select className='select h-9 min-w-0 cursor-pointer !appearance-none truncate !border-0 !bg-transparent !bg-none !pe-1 !ps-2 text-end text-sm focus:!border-0 focus:!shadow-none focus:!outline-none focus:!ring-0'>
<select className='select h-9 min-w-0 cursor-pointer truncate border-0! bg-transparent! bg-none! pe-1! ps-2! text-end text-sm focus:border-0! focus:shadow-none! focus:outline-hidden! focus:ring-0! open:outline-hidden!'>
{/* options */}
</select>
<MdArrowDropDown
aria-hidden='true'
className='text-base-content/55 pointer-events-none h-5 w-5 flex-shrink-0'
className='text-base-content/55 pointer-events-none h-5 w-5 shrink-0'
/>
</div>
```
> **Why so many `!` overrides?** daisyui's `.select` and `.input` apply
> `border-width: 1px` + `border-color` (transparent at rest, `var(--bc)` on
> `border-width: 1px` + `border-color` (base-content/20 at rest, `var(--color-base-content)` on
> focus), plus `outline`, `box-shadow`, and `ring` chrome on focus. To make
> the control truly chromeless inside a boxed list, you need to kill all
> four properties. Missing any of them — especially `border-0` — leaves a
@ -590,7 +592,7 @@ hover/focus bg directly on it. Suppress daisyui's own focus chrome the
same way:
```tsx
<input className='input hover:!bg-base-200/60 focus:!bg-base-200/60 h-9 max-w-[60%] rounded-md !border-0 !bg-transparent !pe-0 !ps-2 text-end text-sm focus:!border-0 focus:!shadow-none focus:!outline-none focus:!ring-0' />
<input className='input hover:bg-base-200/60! focus:bg-base-200/60! h-9 max-w-[60%] rounded-md border-0! bg-transparent! pe-0! ps-2! text-end text-sm focus:border-0! focus:shadow-none! focus:outline-hidden! focus:ring-0!' />
```
> **Why no ring here when §2.7 says "focus needs a visible ring"?** §2.7 is
@ -716,7 +718,7 @@ prefixes.
- Every focusable element must have a visible focus indicator.
- Custom buttons:
`focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-base-content/15`.
`focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-base-content/15`.
- Inputs: rely on daisyui's input focus ring; inputs with custom styling use
`focus:ring-2 focus:ring-primary/40`.
- Don't use `outline-none` without `focus-visible:` replacement.

View file

@ -4,7 +4,7 @@
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"config": "",
"css": "src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,

View file

@ -226,7 +226,8 @@
"@next/bundle-analyzer": "^15.4.2",
"@playwright/test": "^1.60.0",
"@sentry/cli": "^2.39.1",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/postcss": "^4.3.3",
"@tailwindcss/typography": "^0.5.20",
"@tauri-apps/cli": "2.11.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
@ -257,10 +258,9 @@
"@wdio/mocha-framework": "^9.24.0",
"@wdio/spec-reporter": "^9.24.0",
"@wdio/types": "^9.24.0",
"autoprefixer": "^10.4.20",
"caniuse-lite": "^1.0.30001746",
"cpx2": "^8.0.0",
"daisyui": "^4.12.24",
"daisyui": "^5.7.22",
"dotenv-cli": "^7.4.4",
"fast-check": "^4.8.0",
"i18next-scanner": "^4.6.0",
@ -274,7 +274,7 @@
"raw-loader": "^4.0.2",
"react-server-dom-webpack": "^19.2.8",
"serwist": "^9.3.0",
"tailwindcss": "^3.4.18",
"tailwindcss": "^4.3.3",
"typescript": "^5.7.2",
"vinext": "^0.0.21",
"vite": "^7.3.3",

View file

@ -1,8 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
'@tailwindcss/postcss': {},
},
};

View file

@ -584,7 +584,7 @@
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -685,7 +685,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -776,7 +776,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -799,7 +799,7 @@
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
INFOPLIST_FILE = ReadestWidget/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -827,7 +827,7 @@
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
INFOPLIST_FILE = ShareExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -855,7 +855,7 @@
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
INFOPLIST_FILE = ReadestWidget/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -2,7 +2,7 @@ name: Readest
options:
bundleIdPrefix: com.bilingify.readest
deploymentTarget:
iOS: 15.0
iOS: 16.4
fileGroups: [../../src]
packages:
Sentry:
@ -191,7 +191,7 @@ targets:
# file was modified during the build", fix it by adding the
# auto-derived keys to the source file, not by re-enabling this flag.
SWIFT_VERSION: "5.0"
IPHONEOS_DEPLOYMENT_TARGET: 15.0
IPHONEOS_DEPLOYMENT_TARGET: 16.4
ARCHS: [arm64]
VALID_ARCHS: arm64
EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64
@ -224,7 +224,7 @@ targets:
# com.apple.security.application-groups, so the widget reads an empty
# snapshot from the App Group and shows only the placeholder book icon.
SWIFT_VERSION: "5.0"
IPHONEOS_DEPLOYMENT_TARGET: 15.0
IPHONEOS_DEPLOYMENT_TARGET: 16.4
ARCHS: [arm64]
VALID_ARCHS: arm64
EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64

@ -1 +1 @@
Subproject commit c7c04abee8a12e32823febec44779c075e076e25
Subproject commit bbb5bd33ef4ab644e915f7264ef15a8da97cb284

View file

@ -75,7 +75,7 @@
"iOS": {
"developmentTeam": "J5W48D69VR",
"infoPlist": "./Info-ios.plist",
"minimumSystemVersion": "15.0"
"minimumSystemVersion": "16.4"
},
"fileAssociations": [
{
@ -228,7 +228,7 @@
},
"webview-upgrade": {
"minUpgradeMajor": 121,
"minSupportedMajor": 92
"minSupportedMajor": 111
}
}
}

View file

@ -122,9 +122,9 @@ describe('BookDetailView delete dropdown layout', () => {
const menu = container.querySelector('.delete-menu');
expect(menu).toBeTruthy();
expect(menu!.className).not.toContain('dropdown-center');
// It should keep position: relative via the !relative override so it
// It should keep position: relative via the relative! override so it
// anchors against the centered parent.
expect(menu!.className).toContain('!relative');
expect(menu!.className).toContain('relative!');
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

View file

@ -123,8 +123,11 @@ const expectElement = (locator: unknown) =>
* where the triangle points up and highlight options float above.
*/
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
// One of the app's own themes, not daisyUI's stock `dark`: the stock palette
// is daisyUI's to change between releases (it did in v5), while the app's
// themes are pinned in themes.ts.
<div
data-theme='dark'
data-theme='default-dark'
style={{
position: 'relative',
width: POPUP_W,

View file

@ -0,0 +1,137 @@
/**
* Guards the daisyUI 5 migration end to end through the compiled stylesheet:
* every built-in theme lands as v5 `--color-*` tokens on `[data-theme]`, the
* v4 field metrics the layouts were tuned on stay pinned, and `.select` opts
* into `appearance: base-select` so the option popup is painted by the page
* instead of the OS (#5587: white native popups on Windows dark mode).
*/
import { describe, it, expect, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/react';
import { themes, applyCustomTheme } from '@/styles/themes';
import SettingsSelect from '@/components/settings/primitives/SettingsSelect';
await import('@/styles/globals.css');
afterEach(() => {
cleanup();
document.documentElement.removeAttribute('data-theme');
});
const rgb = (hex: string) => {
const n = parseInt(hex.slice(1), 16);
return `rgb(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255})`;
};
describe('daisyUI 5 theme tokens', () => {
it('compiles every built-in theme into --color-* tokens', () => {
for (const theme of themes) {
for (const scheme of ['light', 'dark'] as const) {
document.documentElement.setAttribute('data-theme', `${theme.name}-${scheme}`);
const { container } = render(<div className='bg-base-100 text-primary' />);
const style = getComputedStyle(container.firstElementChild as HTMLElement);
expect(style.backgroundColor, `${theme.name}-${scheme} base-100`).toBe(
rgb(theme.colors[scheme]['base-100']),
);
expect(style.color, `${theme.name}-${scheme} primary`).toBe(
rgb(theme.colors[scheme].primary),
);
expect(getComputedStyle(document.documentElement).colorScheme).toBe(scheme);
cleanup();
}
}
});
it('applies runtime custom themes through the same tokens', () => {
applyCustomTheme({
name: 'custom-test',
label: 'Custom',
colors: {
light: { bg: '#123456', fg: '#ffffff', primary: '#00ff00' },
dark: { bg: '#654321', fg: '#000000', primary: '#ff00ff' },
},
});
document.documentElement.setAttribute('data-theme', 'custom-test-dark');
const { container } = render(<div className='bg-base-100 text-primary' />);
const style = getComputedStyle(container.firstElementChild as HTMLElement);
expect(style.backgroundColor).toBe('rgb(101, 67, 33)');
expect(style.color).toBe('rgb(255, 0, 255)');
expect(getComputedStyle(document.documentElement).colorScheme).toBe('dark');
document.getElementById('theme-custom-test-styles')?.remove();
});
it('keeps the daisyUI 4 field metrics', () => {
document.documentElement.setAttribute('data-theme', 'default-light');
const { getByTestId } = render(
<>
<button data-testid='btn' className='btn'>
md
</button>
<button data-testid='btn-sm' className='btn btn-sm'>
sm
</button>
<input data-testid='input' className='input' />
<select data-testid='select' className='select'>
<option>x</option>
</select>
<input data-testid='checkbox' type='checkbox' className='checkbox' />
<input data-testid='toggle' type='checkbox' className='toggle' defaultChecked />
<input data-testid='toggle-sm' type='checkbox' className='toggle toggle-sm' />
<input data-testid='toggle-xs' type='checkbox' className='toggle toggle-xs' />
</>,
);
const box = (id: string) => getByTestId(id).getBoundingClientRect();
expect(box('btn').height).toBe(48);
expect(box('btn-sm').height).toBe(32);
expect(getComputedStyle(getByTestId('btn-sm')).fontSize).toBe('14px');
expect(box('input').height).toBe(48);
expect(box('select').height).toBe(48);
expect(getComputedStyle(getByTestId('btn')).borderTopLeftRadius).toBe('8px');
expect(getComputedStyle(getByTestId('checkbox')).borderTopLeftRadius).toBe('8px');
// daisyUI 5 narrowed the toggle track from 3rem to 2.5rem; the switch reads
// as base-100 track + base-content knob on both versions.
const toggle = getByTestId('toggle');
expect(box('toggle').width).toBe(48);
expect(box('toggle').height).toBe(24);
expect(getComputedStyle(toggle).backgroundColor).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(toggle, '::before').backgroundColor).toBe('rgb(23, 23, 23)');
// Each size has its own v4 width, so the base pin must not flatten them.
expect([box('toggle-sm').width, box('toggle-sm').height]).toEqual([32, 20]);
expect([box('toggle-xs').width, box('toggle-xs').height]).toEqual([24, 16]);
});
it('keeps the Tailwind 3 default palette', () => {
// Tailwind 4 ships an oklch palette that renders noticeably more vivid; the
// app pins the v3 values (globals.css @theme) so no page changes color.
const { getByTestId } = render(
<>
<div data-testid='green' className='bg-green-500' />
<div data-testid='gray' className='text-gray-500' />
<div data-testid='red' className='bg-red-500' />
</>,
);
expect(getComputedStyle(getByTestId('green')).backgroundColor).toBe('rgb(34, 197, 94)');
expect(getComputedStyle(getByTestId('gray')).color).toBe('rgb(107, 114, 128)');
expect(getComputedStyle(getByTestId('red')).backgroundColor).toBe('rgb(239, 68, 68)');
});
it('renders the select picker in-page (#5587)', () => {
const { getByTestId, getByLabelText } = render(
<>
<select data-testid='select' className='select'>
<option>x</option>
</select>
<SettingsSelect
value='a'
onChange={() => {}}
options={[{ value: 'a', label: 'A' }]}
ariaLabel='Settings select'
/>
</>,
);
expect(getComputedStyle(getByTestId('select')).appearance).toBe('base-select');
// The settings primitive strips daisyUI's chevron and chrome; it must not
// also strip the appearance, or the popup falls back to the OS one.
expect(getComputedStyle(getByLabelText('Settings select')).appearance).toBe('base-select');
});
});

View file

@ -67,7 +67,7 @@ const renderMenu = (
>
<Menu className={`dropdown-content no-triangle z-20 mt-2 shadow-2xl ${menuClassName}`}>
{labels.map((label) => (
<MenuItem key={label} label={label} buttonClass='min-h-8 !py-1' transient />
<MenuItem key={label} label={label} buttonClass='min-h-8 py-1!' transient />
))}
</Menu>
</Dropdown>

View file

@ -0,0 +1,70 @@
/**
* FontDropdown's "System Fonts" sub-list is a nested daisyUI dropdown. It must
* open only when the user activates the "System Fonts" row (daisyUI 4 showed it
* alongside the parent list), and the parent list must keep a real width: the
* rows are `w-full overflow-hidden`, so a shrink-to-fit menu collapses to 0px.
*/
import { describe, it, expect, afterEach, vi } from 'vitest';
import { render, cleanup } from '@testing-library/react';
import { page } from 'vitest/browser';
vi.mock('@/context/EnvContext', () => ({
useEnv: () => ({ envConfig: {}, appService: null }),
}));
vi.mock('@/hooks/useTranslation', () => ({
useTranslation: () => (s: string) => s,
}));
vi.mock('@/hooks/useResponsiveSize', () => ({
useResponsiveSize: (n: number) => n,
useDefaultIconSize: () => 20,
}));
const { default: FontDropdown } = await import('@/components/settings/FontDropDown');
await import('@/styles/globals.css');
afterEach(() => cleanup());
const renderDropdown = () => {
const onSelect = vi.fn();
render(
<div style={{ padding: 400 }}>
<FontDropdown
family='sans-serif'
selected='Roboto'
options={[{ option: 'Roboto' }, { option: 'Noto Sans' }]}
moreOptions={[{ option: 'Arial' }, { option: 'Avenir' }]}
onSelect={onSelect}
onGetFontFamily={(option) => option}
/>
</div>,
);
const menus = document.querySelectorAll<HTMLElement>('.dropdown-content');
return { onSelect, list: menus[0]!, subList: menus[1]! };
};
const visible = (el: HTMLElement) => getComputedStyle(el).visibility === 'visible';
describe('FontDropdown system fonts sub-list', () => {
it('opens the sub-list only from the System Fonts row', async () => {
const { onSelect, list, subList } = renderDropdown();
expect(visible(list)).toBe(false);
await page.elementLocator(document.querySelector('button.btn')!).click();
expect(visible(list)).toBe(true);
expect(list.getBoundingClientRect().width).toBeGreaterThan(100);
expect(visible(subList)).toBe(false);
await page.getByText('System Fonts').click();
expect(visible(list)).toBe(true);
expect(visible(subList)).toBe(true);
// The sub-list sits beside the parent list with both bottom edges flush.
expect(
Math.abs(subList.getBoundingClientRect().bottom - list.getBoundingClientRect().bottom),
).toBeLessThanOrEqual(1);
expect(subList.getBoundingClientRect().right).toBeLessThan(list.getBoundingClientRect().left);
await page.getByRole('button', { name: 'Avenir' }).click();
expect(onSelect).toHaveBeenCalledWith('Avenir');
});
});

View file

@ -0,0 +1,33 @@
import { describe, it, expect, vi } from 'vitest';
import daisyuiThemes from '@/styles/daisyui-themes';
import { themes, themeVariables } from '@/styles/themes';
/**
* The built-in Readest themes reach Tailwind through `@plugin './daisyui-themes.ts'`
* in globals.css (Tailwind 4 has no JS config). Every theme must compile to a
* `[data-theme="<name>-<scheme>"]` block carrying the same tokens the runtime
* injector emits for custom themes, so both paths render identically.
*/
describe('daisyui-themes plugin', () => {
const collect = () => {
const addBase = vi.fn();
daisyuiThemes.handler({ addBase } as never);
return Object.assign({}, ...addBase.mock.calls.map(([styles]) => styles)) as Record<
string,
Record<string, string>
>;
};
it('emits a light and a dark block for each built-in theme', () => {
const base = collect();
for (const theme of themes) {
expect(base[`[data-theme="${theme.name}-light"]`]).toEqual(
themeVariables(theme.colors.light, 'light'),
);
expect(base[`[data-theme="${theme.name}-dark"]`]).toEqual(
themeVariables(theme.colors.dark, 'dark'),
);
}
expect(Object.keys(base)).toHaveLength(themes.length * 2);
});
});

View file

@ -1,45 +1,16 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
import {
hexToOklch,
getContrastOklch,
getContrastHex,
generateLightPalette,
generateDarkPalette,
themeVariables,
applyCustomTheme,
type BaseColor,
type CustomTheme,
type Palette,
} from '@/styles/themes';
import tinycolor from 'tinycolor2';
describe('hexToOklch', () => {
it('should convert red (#ff0000) to oklch string', () => {
const result = hexToOklch('#ff0000');
// Red in oklch should have high lightness, high chroma, hue around 29deg
expect(result).toMatch(/^\d+\.\d+%\s+[\d.]+\s+[\d.]+deg$/);
const parts = result.split(/[\s%]+/);
const lightness = parseFloat(parts[0]!);
expect(lightness).toBeGreaterThan(50);
expect(lightness).toBeLessThan(70);
});
it('should convert black (#000000) to oklch with 0% lightness', () => {
const result = hexToOklch('#000000');
expect(result).toMatch(/^0\.0000%\s+0\s+0deg$/);
});
it('should convert white (#ffffff) to oklch with ~100% lightness', () => {
const result = hexToOklch('#ffffff');
expect(result).toMatch(/^100\.0000%\s+0\s+0deg$/);
});
it('should produce achromatic output for gray colors', () => {
const result = hexToOklch('#808080');
// Gray should have very low chroma
const parts = result.split(/[\s%]+/);
const chroma = parseFloat(parts[1]!);
expect(chroma).toBeLessThan(0.001);
});
});
describe('getContrastHex', () => {
it('should return white for dark colors', () => {
expect(getContrastHex('#000000').toLowerCase()).toBe('#ffffff');
@ -54,18 +25,6 @@ describe('getContrastHex', () => {
});
});
describe('getContrastOklch', () => {
it('should return white oklch (100% 0 0deg) for dark colors', () => {
expect(getContrastOklch('#000000')).toBe('100% 0 0deg');
expect(getContrastOklch('#333333')).toBe('100% 0 0deg');
});
it('should return black oklch (0% 0 0deg) for light colors', () => {
expect(getContrastOklch('#ffffff')).toBe('0% 0 0deg');
expect(getContrastOklch('#f0f0f0')).toBe('0% 0 0deg');
});
});
describe('generateLightPalette', () => {
const lightColors: BaseColor = {
bg: '#ffffff',
@ -216,3 +175,87 @@ describe('palette contrast', () => {
}
});
});
describe('themeVariables', () => {
const palette = generateLightPalette({ bg: '#ffffff', fg: '#171717', primary: '#0066cc' });
it('emits daisyUI 5 color tokens as plain colors', () => {
const vars = themeVariables(palette, 'light');
expect(vars['color-scheme']).toBe('light');
expect(vars['--color-base-100']).toBe('#ffffff');
expect(vars['--color-base-200']).toBe(palette['base-200']);
expect(vars['--color-base-300']).toBe(palette['base-300']);
expect(vars['--color-base-content']).toBe('#171717');
expect(vars['--color-primary']).toBe('#0066cc');
expect(vars['--color-secondary']).toBe(palette.secondary);
expect(vars['--color-accent']).toBe(palette.accent);
expect(vars['--color-neutral']).toBe(palette.neutral);
expect(vars['--color-neutral-content']).toBe(palette['neutral-content']);
for (const state of ['info', 'success', 'warning', 'error']) {
expect(vars[`--color-${state}`]).toMatch(/^oklch\(/);
expect(vars[`--color-${state}-content`]).toMatch(/^oklch\(/);
}
});
it('derives readable *-content colors like daisyUI 4 did (80% toward white or black)', () => {
const vars = themeVariables(palette, 'light');
// #0066cc is dark, so its content color leans to white; the accent/secondary
// of this palette are light, so theirs lean to black.
expect(tinycolor(vars['--color-primary-content']!).isLight()).toBe(true);
expect(tinycolor(vars['--color-primary-content']!).toHexString()).not.toBe('#ffffff');
expect(tinycolor(vars['--color-secondary-content']!).isDark()).toBe(true);
});
it('pins the daisyUI 4 shape so components keep their radii and flat look', () => {
const vars = themeVariables(palette, 'dark');
expect(vars['color-scheme']).toBe('dark');
expect(vars['--radius-field']).toBe('0.5rem');
expect(vars['--radius-box']).toBe('1rem');
expect(vars['--radius-selector']).toBe('1.9rem');
expect(vars['--depth']).toBe('0');
expect(vars['--noise']).toBe('0');
expect(vars['--border']).toBe('1px');
});
});
describe('applyCustomTheme', () => {
const customTheme: CustomTheme = {
name: 'custom-1',
label: 'Custom',
colors: {
light: { bg: '#fefefe', fg: '#111111', primary: '#0066cc' },
dark: { bg: '#101010', fg: '#eeeeee', primary: '#77bbee' },
},
};
afterEach(() => {
document.getElementById('theme-custom-1-styles')?.remove();
});
it('injects one [data-theme] block per scheme with daisyUI 5 tokens', () => {
const names = applyCustomTheme(customTheme);
expect(names).toEqual({ light: 'custom-1-light', dark: 'custom-1-dark' });
const style = document.getElementById('theme-custom-1-styles') as HTMLStyleElement;
expect(style.tagName).toBe('STYLE');
const css = style.textContent!;
expect(css).toContain('[data-theme="custom-1-light"]');
expect(css).toContain('[data-theme="custom-1-dark"]');
expect(css).toMatch(/custom-1-light"\]\s*\{[^}]*color-scheme:\s*light/);
expect(css).toMatch(/custom-1-dark"\]\s*\{[^}]*color-scheme:\s*dark/);
expect(css).toMatch(/custom-1-light"\]\s*\{[^}]*--color-base-100:\s*#fefefe/);
expect(css).toMatch(/custom-1-dark"\]\s*\{[^}]*--color-base-100:\s*#101010/);
expect(css).not.toContain('--b1:');
expect(css).not.toContain('--fallback-');
});
it('replaces the previous style element for the same theme', () => {
applyCustomTheme(customTheme);
applyCustomTheme({
...customTheme,
colors: { ...customTheme.colors, light: { ...customTheme.colors.light, bg: '#f0f0f0' } },
});
const styles = document.querySelectorAll('#theme-custom-1-styles');
expect(styles).toHaveLength(1);
expect(styles[0]!.textContent).toContain('--color-base-100: #f0f0f0');
});
});

View file

@ -138,9 +138,9 @@ export default function EmailPasswordAuth({
onSubmit={handleSubmit}
className='w-full space-y-4'
>
<div className='form-control'>
<label className='label' htmlFor='email'>
<span className='label-text'>{_('Email address')}</span>
<div className='flex flex-col'>
<label className='flex select-none items-center justify-between px-1 py-2' htmlFor='email'>
<span className='text-sm'>{_('Email address')}</span>
</label>
<input
id='email'
@ -150,15 +150,18 @@ export default function EmailPasswordAuth({
defaultValue={defaultEmail}
placeholder={_('Your email address')}
autoComplete={hasPassword ? 'username' : 'email'}
className='input input-bordered eink-bordered w-full rounded-lg placeholder:text-sm'
className='input eink-bordered w-full rounded-lg placeholder:text-sm'
disabled={loading}
onFocus={keepAboveKeyboard}
/>
</div>
{hasPassword && (
<div className='form-control'>
<label className='label' htmlFor='password'>
<span className='label-text'>
<div className='flex flex-col'>
<label
className='flex select-none items-center justify-between px-1 py-2'
htmlFor='password'
>
<span className='text-sm'>
{view === 'sign_in' ? _('Your Password') : _('Create a Password')}
</span>
</label>
@ -169,7 +172,7 @@ export default function EmailPasswordAuth({
required
placeholder={_('Your password')}
autoComplete={view === 'sign_in' ? 'current-password' : 'new-password'}
className='input input-bordered eink-bordered w-full rounded-lg placeholder:text-sm'
className='input eink-bordered w-full rounded-lg placeholder:text-sm'
disabled={loading}
onFocus={keepAboveKeyboard}
/>

View file

@ -30,7 +30,7 @@ export const ProviderLogin: React.FC<ProviderLoginProp> = ({
'transition-colors duration-150',
'hover:border-base-300 hover:bg-base-200/60',
'active:bg-base-200/80',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<Icon className='h-5 w-5' aria-hidden='true' />

View file

@ -69,7 +69,7 @@ export default function UpdateEmailPage() {
placeholder={_('Your new email')}
required
disabled={loading}
className={`w-full rounded-md border bg-transparent px-4 py-2.5 focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 ${isDarkMode ? 'text-gray-300' : 'text-gray-400'}`}
className={`w-full rounded-md border bg-transparent px-4 py-2.5 focus:outline-hidden focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 ${isDarkMode ? 'text-gray-300' : 'text-gray-400'}`}
/>
</div>

View file

@ -180,7 +180,7 @@ export const BackupWindow: React.FC<BackupWindowProps> = ({ onPullLibrary }) =>
onClose={handleClose}
snapHeight={appService?.isMobile ? 0.45 : undefined}
dismissible={!isProcessing}
boxClassName='sm:!w-[520px] sm:!max-w-screen-sm sm:h-auto'
boxClassName='sm:w-[520px]! sm:max-w-(--breakpoint-sm)! sm:h-auto'
>
{isOpen && (
<div className='backup-content flex flex-col gap-6 px-6 py-4'>

View file

@ -93,7 +93,7 @@ const BookContextMenuPopup: React.FC<BookContextMenuPopupProps> = ({
style={{ left: placement?.left ?? 0, top: placement?.top ?? 0 }}
>
<Menu
className='dropdown-content no-triangle bg-base-100 rounded-box !relative z-[1] !mt-0 p-2 shadow'
className='dropdown-content no-triangle bg-base-100 rounded-box relative! z-[1] mt-0! p-2 shadow-sm'
onCancel={onClose}
>
{items.map(({ text, action }) => (

View file

@ -112,7 +112,7 @@ const BookItem: React.FC<BookItemProps> = ({
>
<div
className={clsx(
'bookitem-main relative flex justify-center overflow-hidden rounded',
'bookitem-main relative flex justify-center overflow-hidden rounded-sm',
!fitCoverInGrid && 'aspect-[28/41]',
coverFit === 'crop' && 'shadow-md',
mode === 'grid' && 'items-end',
@ -127,7 +127,7 @@ const BookItem: React.FC<BookItemProps> = ({
showSpine={settings.librarySkeuomorphicCovers}
imageClassName={clsx(
'shadow-md',
settings.librarySkeuomorphicCovers ? 'rounded-none' : 'rounded',
settings.librarySkeuomorphicCovers ? 'rounded-none' : 'rounded-sm',
)}
onAspectRatioChange={setCoverAspect}
/>
@ -146,7 +146,7 @@ const BookItem: React.FC<BookItemProps> = ({
{isIndeterminate ? (
<span className='loading loading-spinner loading-sm text-white eink:text-base-content' />
) : (
<span className='eink:text-base-content text-sm font-semibold text-white not-eink:drop-shadow-sm'>
<span className='eink:text-base-content text-sm font-semibold text-white not-eink:drop-shadow-xs'>
{Math.round(transferProgress)}%
</span>
)}
@ -160,7 +160,7 @@ const BookItem: React.FC<BookItemProps> = ({
{bookSelected ? (
<MdCheckCircle className='fill-blue-500' />
) : (
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-xs' />
)}
</div>
)}
@ -176,7 +176,7 @@ const BookItem: React.FC<BookItemProps> = ({
<h4
className={clsx(
'overflow-hidden text-ellipsis font-semibold',
mode === 'grid' && 'block whitespace-nowrap text-[0.6em] text-xs',
mode === 'grid' && 'block whitespace-nowrap text-xs',
mode === 'list' && 'line-clamp-1 text-base',
)}
>

View file

@ -1028,7 +1028,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
tabIndex={-1}
role='main'
aria-label={_('Bookshelf')}
className='bookshelf flex min-h-0 flex-grow flex-col focus:outline-none'
className='bookshelf flex min-h-0 grow flex-col focus:outline-hidden'
>
{!contentSearch?.query.trim() && queryTerm && (
<div className='flex shrink-0 justify-center px-4 pb-2'>
@ -1039,7 +1039,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
'eink-bordered border-base-200 bg-base-100 hover:border-base-300 hover:bg-base-300/40',
'text-base-content/80 hover:text-base-content not-eink:transition-colors',
'flex h-9 items-center gap-2 rounded-lg border px-4 text-sm font-medium duration-150',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<MdManageSearch aria-hidden='true' className='h-5 w-5' />

View file

@ -164,7 +164,7 @@ export const CacheManagerWindow = () => {
const ghostBtn = clsx(
'eink-bordered flex h-11 items-center justify-center rounded-xl border border-transparent',
'text-base-content hover:bg-base-200 text-sm font-medium transition-colors',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
'disabled:opacity-40',
);
@ -194,7 +194,7 @@ export const CacheManagerWindow = () => {
isOpen={isOpen}
title={_('Manage Cache')}
onClose={handleClose}
boxClassName='sm:!w-[440px] sm:!max-w-screen-sm sm:h-auto'
boxClassName='sm:w-[440px]! sm:max-w-(--breakpoint-sm)! sm:h-auto'
>
{isOpen && (
<div className='cache-manager-content flex flex-col gap-7 px-2 pb-2 pt-3'>

View file

@ -29,8 +29,8 @@ const FailedImportsDialog: React.FC<FailedImportsDialogProps> = ({ failedImports
isOpen
title={_('Failed to import {{count}} books', { count: failedImports.length })}
onClose={onClose}
boxClassName='sm:min-w-[440px] sm:max-w-[460px] sm:!h-auto sm:max-h-[80%]'
contentClassName='!my-0 !px-5 !pt-0 !pb-4 !flex-grow-0'
boxClassName='sm:min-w-[440px] sm:max-w-[460px] sm:h-auto! sm:max-h-[80%]'
contentClassName='my-0! px-5! pt-0! pb-4! grow-0!'
>
<div className='flex flex-col gap-3'>
<div
@ -39,7 +39,7 @@ const FailedImportsDialog: React.FC<FailedImportsDialogProps> = ({ failedImports
'bg-error/8 text-base-content border-error/15 border px-3.5 py-2.5',
)}
>
<MdErrorOutline className='text-error h-5 w-5 flex-shrink-0' aria-hidden='true' />
<MdErrorOutline className='text-error h-5 w-5 shrink-0' aria-hidden='true' />
<p className='text-[0.85em] leading-snug'>{subtitle}</p>
</div>
@ -52,7 +52,7 @@ const FailedImportsDialog: React.FC<FailedImportsDialogProps> = ({ failedImports
{failedImports.map((item, index) => (
<li key={`${item.filename}-${index}`} className='flex items-center gap-2.5 px-3 py-2'>
<MdInsertDriveFile
className='text-base-content/40 h-4 w-4 flex-shrink-0'
className='text-base-content/40 h-4 w-4 shrink-0'
aria-hidden='true'
/>
<div className='flex min-w-0 flex-1 flex-col'>

View file

@ -103,7 +103,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
<div className={clsx('group-item', appService?.hasContextMenu ? 'cursor-pointer' : '')}>
<div
className={clsx(
'groupitem-main relative flex overflow-hidden rounded',
'groupitem-main relative flex overflow-hidden rounded-sm',
mode === 'grid' && 'bg-base-100 aspect-[28/41] items-center justify-center shadow-md',
mode === 'list' && 'items-center justify-start gap-4 py-2',
)}
@ -140,7 +140,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
className={clsx(
'relative aspect-[28/41] h-full',
mode === 'grid' && 'w-full',
mode === 'list' && 'flex-shrink-0',
mode === 'list' && 'shrink-0',
)}
>
<BookCover
@ -154,7 +154,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
</div>
{mode === 'list' && showLeftArrow && (
<div className='absolute left-[-0.5px] top-0 h-full w-12'>
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-r to-transparent'></div>
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-linear-to-r to-transparent'></div>
<button
aria-label={_('Scroll left')}
onClick={handleLeftArrowClick}
@ -165,7 +165,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
onPointerLeave={(e) => stopEvent(e)}
className='absolute left-2 top-1/2 -translate-y-1/2 transition-all duration-200 hover:scale-110'
>
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-sm transition-colors duration-200'>
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-xs transition-colors duration-200'>
<MdChevronLeft
size={16}
className='text-base-content/50 hover:text-base-content/70'
@ -176,7 +176,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
)}
{mode === 'list' && showRightArrow && (
<div className='absolute right-[-0.5px] top-0 h-full w-12'>
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-l to-transparent'></div>
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-linear-to-l to-transparent'></div>
<button
aria-label={_('Scroll right')}
onClick={handleRightArrowClick}
@ -187,7 +187,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
onPointerLeave={(e) => stopEvent(e)}
className='absolute right-2 top-1/2 -translate-y-1/2 transition-all duration-200 hover:scale-110'
>
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-sm transition-colors duration-200'>
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-xs transition-colors duration-200'>
<MdChevronRight
size={16}
className='text-base-content/50 hover:text-base-content/70'
@ -210,7 +210,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
{groupSelected ? (
<MdCheckCircle className='fill-blue-500' />
) : (
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-xs' />
)}
</div>
)}

View file

@ -305,13 +305,13 @@ const GroupingModal: React.FC<GroupingModalProps> = ({
}
e.stopPropagation();
}}
className='input input-ghost w-full border-0 px-2 text-base !outline-none sm:text-sm'
className='input input-ghost w-full border-0 px-2 text-base outline-hidden! sm:text-sm'
/>
<button
className={clsx(
'btn btn-ghost settings-content hover:bg-transparent',
'flex h-[1.3em] min-h-[1.3em] items-end p-0',
editGroupName ? '' : 'btn-disabled !bg-opacity-0',
editGroupName ? '' : 'btn-disabled bg-transparent!',
)}
onClick={() => handleConfirmCreateGroup()}
>
@ -328,13 +328,13 @@ const GroupingModal: React.FC<GroupingModalProps> = ({
<div className='mt-4 flex flex-wrap items-center gap-2 text-base'>
<button
onClick={handleNavigateBack}
className='hover:bg-base-300 flex items-center gap-1 rounded px-2 py-1'
className='hover:bg-base-300 flex items-center gap-1 rounded-sm px-2 py-1'
>
<IoMdArrowBack size={iconSize} />
</button>
<button
onClick={() => handleNavigateToPath(undefined)}
className='hover:bg-base-300 rounded px-2 py-1'
className='hover:bg-base-300 rounded-sm px-2 py-1'
>
{_('All')}
</button>
@ -344,11 +344,11 @@ const GroupingModal: React.FC<GroupingModalProps> = ({
<React.Fragment key={index}>
<MdChevronRight size={iconSize} className='text-neutral-content' />
{isLast ? (
<span className='truncate rounded px-2 py-1'>{crumb.name}</span>
<span className='truncate rounded-sm px-2 py-1'>{crumb.name}</span>
) : (
<button
onClick={() => handleNavigateToPath(crumb.path)}
className='hover:bg-base-300 truncate rounded px-2 py-1'
className='hover:bg-base-300 truncate rounded-sm px-2 py-1'
>
{crumb.name}
</button>

View file

@ -328,7 +328,7 @@ const ImportFromFolderDialog: React.FC<ImportFromFolderDialogProps> = ({
title={_('Import Books')}
onClose={onCancel}
boxClassName={DIALOG_BOX_CLASS}
contentClassName='!px-6 !py-2'
contentClassName='px-6! py-2!'
>
<WatchedFoldersPane
folders={watchedFolders}
@ -356,7 +356,7 @@ const ImportFromFolderDialog: React.FC<ImportFromFolderDialogProps> = ({
title={_('Import Books')}
onClose={onCancel}
boxClassName={DIALOG_BOX_CLASS}
contentClassName='!px-6 !py-2'
contentClassName='px-6! py-2!'
>
<div className='flex flex-col gap-4 pt-2'>
{/* Directory row — clickable input that pops the native folder
@ -373,13 +373,13 @@ const ImportFromFolderDialog: React.FC<ImportFromFolderDialogProps> = ({
'eink-bordered flex w-full items-center gap-2 rounded-lg px-3 py-2.5',
'text-start text-sm transition-colors duration-150',
'border-base-300 bg-base-200/40 hover:bg-base-200/70',
'focus-visible:ring-primary/40 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-primary/40 focus-visible:outline-hidden focus-visible:ring-2',
picking && 'opacity-60',
)}
title={directory || _('Choose a folder')}
aria-label={_('Choose a folder')}
>
<MdFolderOpen className='text-base-content/70 h-5 w-5 flex-shrink-0' />
<MdFolderOpen className='text-base-content/70 h-5 w-5 shrink-0' />
<span className={clsx('min-w-0 flex-1 truncate', !directory && 'text-base-content/50')}>
{directory || _('Choose a folder')}
</span>
@ -443,7 +443,7 @@ const ImportFromFolderDialog: React.FC<ImportFromFolderDialogProps> = ({
'no-spinner',
'h-full min-w-0 flex-1 rounded-s-lg bg-transparent',
'ps-2 pe-1 text-end text-sm',
'focus:outline-none',
'focus:outline-hidden',
)}
aria-label={_('Minimum file size (KB)')}
/>

View file

@ -69,7 +69,7 @@ const ImportFromUrlDialog: React.FC<ImportFromUrlDialogProps> = ({ isOpen, onClo
// `sm:h-[65%]` so we don't end up with a near-full-height modal
// for a one-line form. Width-constrained to a comfortable
// reading measure.
boxClassName='sm:!w-[480px] sm:!max-w-[480px] sm:!h-auto sm:!max-h-[80vh]'
boxClassName='sm:w-[480px]! sm:max-w-[480px]! sm:h-auto! sm:max-h-[80vh]!'
>
<div className='flex flex-col gap-4 pb-6 pt-2'>
<p className='text-base-content/60 text-sm leading-relaxed'>
@ -78,10 +78,10 @@ const ImportFromUrlDialog: React.FC<ImportFromUrlDialogProps> = ({ isOpen, onClo
<input
type='url'
autoFocus
// Explicit placeholder colour — daisyUI's `input-bordered`
// Explicit placeholder colour — daisyUI's input border
// leaves placeholders too dark on light themes; the user
// can mistake the example for actual content.
className='input input-bordered eink-bordered placeholder:text-base-content/35 w-full'
className='input eink-bordered placeholder:text-base-content/35 w-full'
placeholder='https://example.com/article'
value={url}
disabled={submitting}

View file

@ -71,7 +71,7 @@ const ImportMenu: React.FC<ImportMenuProps> = ({
return (
<Menu
className={clsx(
'dropdown-content bg-base-100 rounded-box !relative z-[1] mt-3 p-2 shadow',
'dropdown-content bg-base-100 rounded-box relative! z-[1] mt-3 p-2 shadow-sm',
menuClassName,
)}
onCancel={() => setIsDropdownOpen?.(false)}

View file

@ -73,7 +73,7 @@ const ImportMenuPopup: React.FC<ImportMenuPopupProps> = ({ anchor, onClose, ...m
style={{ left: position?.left ?? 0, top: position?.top ?? 0 }}
>
<ImportMenu
menuClassName='no-triangle !mt-0'
menuClassName='no-triangle mt-0!'
setIsDropdownOpen={(open) => !open && onClose()}
{...menuProps}
/>

View file

@ -135,7 +135,7 @@ const ImportNovelDialog: React.FC<ImportNovelDialogProps> = ({ isOpen, onClose,
onClose={close}
title={_('Import Web Novel')}
// Size to content — same override as ImportFromUrlDialog.
boxClassName='sm:!w-[480px] sm:!max-w-[480px] sm:!h-auto sm:!max-h-[80vh]'
boxClassName='sm:w-[480px]! sm:max-w-[480px]! sm:h-auto! sm:max-h-[80vh]!'
>
<div className='flex flex-col gap-4 pb-6 pt-2'>
{phase === 'url' && (
@ -148,7 +148,7 @@ const ImportNovelDialog: React.FC<ImportNovelDialogProps> = ({ isOpen, onClose,
<input
type='url'
autoFocus
className='input input-bordered eink-bordered placeholder:text-base-content/35 w-full'
className='input eink-bordered placeholder:text-base-content/35 w-full'
placeholder='https://example.com/novel'
value={url}
disabled={busy}

View file

@ -48,7 +48,7 @@ const LibraryEmptyState: React.FC<LibraryEmptyStateProps> = ({ onImport }) => {
className={clsx(
'text-base-content/70 hover:text-base-content mt-1 py-2 text-sm font-medium',
'underline underline-offset-4',
'focus-visible:text-base-content focus-visible:outline-none',
'focus-visible:text-base-content focus-visible:outline-hidden',
)}
onClick={() => navigateToLogin(router)}
>

View file

@ -120,7 +120,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
'text-base-content/55 hover:text-base-content',
'not-eink:transition-colors ms-1.5 flex h-7 min-h-7 items-center justify-center',
'touch-target w-8 rounded-full bg-transparent duration-150',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
)}
onClick={() => onSearchTargetChange(searchTarget === 'text' ? 'books' : 'text')}
>
@ -150,7 +150,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
'bg-base-300/45 border-0',
'font-sans text-sm font-light',
'placeholder:text-base-content/50 truncate',
'focus:outline-none focus:ring-0',
'focus:outline-hidden focus:ring-0',
)}
/>
{searchTarget === 'text' && (
@ -166,7 +166,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
menuClassName='no-triangle mt-1'
buttonClassName={clsx(
'btn btn-ghost h-full min-h-0 w-9 rounded-none rounded-e-full p-0',
'!bg-transparent hover:!bg-transparent',
'bg-transparent! hover:bg-transparent!',
)}
toggleButton={
<FaChevronDown role='none' className='text-base-content/50 h-3 w-3' />
@ -204,7 +204,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
className={clsx(
'exclude-title-bar-mousedown dropdown-bottom dropdown-center cursor-pointer',
)}
buttonClassName='p-0 h-6 min-h-6 w-6 flex touch-target items-center justify-center !bg-transparent'
buttonClassName='p-0 h-6 min-h-6 w-6 flex touch-target items-center justify-center bg-transparent!'
toggleButton={<PiPlus role='none' className='m-0.5 h-5 w-5' />}
>
<ImportMenu

View file

@ -86,7 +86,7 @@ const Excerpt = ({ excerpt }: { excerpt: SearchExcerpt }) => (
const ResultCover = ({ book }: { book: Book }) => (
<span
aria-hidden='true'
className='bg-base-200 eink-bordered relative flex h-10 w-7 shrink-0 items-center justify-center overflow-hidden rounded'
className='bg-base-200 eink-bordered relative flex h-10 w-7 shrink-0 items-center justify-center overflow-hidden rounded-sm'
>
<span className='text-base-content/50 text-[10px] font-semibold'>
{(book.title ?? '').trim().charAt(0)}
@ -130,7 +130,7 @@ const ResultGroupMatches = memo(
type='button'
className={clsx(
'not-eink:transition-colors mx-1.5 block w-[calc(100%-0.75rem)] rounded-lg px-2.5 py-2 text-start duration-150',
'hover:bg-base-200/60 focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset',
'hover:bg-base-200/60 focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset',
'eink:border-base-content/25 eink:border-t eink:first:border-t-0 eink:rounded-none',
)}
onClick={() => onSelectResult(book, match)}
@ -435,7 +435,7 @@ const LibrarySearchResults = ({
})}
className={clsx(
'not-eink:transition-colors flex min-h-14 w-full items-center gap-3 rounded-t-xl px-3 py-2 text-start duration-150',
'hover:bg-base-200/60 focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset',
'hover:bg-base-200/60 focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset',
!isExpanded && 'rounded-b-xl',
)}
onClick={() => toggleBook(group.book.hash)}

View file

@ -282,7 +282,7 @@ export const MigrateDataWindow = () => {
isOpen={isOpen}
title={_('Change Data Location')}
onClose={handleClose}
boxClassName='sm:!w-[520px] sm:!max-w-screen-sm sm:h-auto'
boxClassName='sm:w-[520px]! sm:max-w-(--breakpoint-sm)! sm:h-auto'
>
{isOpen && (
<div className='migrate-data-dir-content flex flex-col gap-6 px-6 py-4'>
@ -296,7 +296,7 @@ export const MigrateDataWindow = () => {
className='bg-base-200 flex w-full items-center gap-2 rounded-lg p-3'
onClick={() => handleRevealDir(currentDataDir)}
>
<RiFolderOpenLine className='text-base-content/70 h-4 w-4 flex-shrink-0' />
<RiFolderOpenLine className='text-base-content/70 h-4 w-4 shrink-0' />
<span className='text-base-content/80 break-all text-start font-mono text-sm'>
{currentDataDir || _('Loading...')}
</span>
@ -325,7 +325,7 @@ export const MigrateDataWindow = () => {
className='bg-primary/10 border-primary/20 flex w-full items-center gap-2 rounded-lg border p-3'
onClick={() => handleRevealDir(newDataDir)}
>
<RiFolderOpenLine className='text-primary h-4 w-4 flex-shrink-0' />
<RiFolderOpenLine className='text-primary h-4 w-4 shrink-0' />
<span className='text-primary break-all text-start font-mono text-sm'>
{newDataDir}
</span>
@ -343,7 +343,7 @@ export const MigrateDataWindow = () => {
<div
className={clsx(
'folder-menu dropdown-content no-triangle left-0',
'border-base-300 !bg-base-200 z-20 mt-1 max-w-[90vw] shadow-2xl',
'border-base-300 bg-base-200! z-20 mt-1 max-w-[90vw] shadow-2xl',
)}
>
{androidNewDirs.map((dir) => (
@ -447,7 +447,7 @@ export const MigrateDataWindow = () => {
{canStartMigration && (
<div className='bg-warning/10 border-warning/20 rounded-lg border p-3'>
<div className='flex items-start gap-2'>
<RiErrorWarningFill className='text-warning mt-0.5 h-4 w-4 flex-shrink-0' />
<RiErrorWarningFill className='text-warning mt-0.5 h-4 w-4 shrink-0' />
<div className='space-y-1'>
<p className='text-base-content text-sm font-medium'>{_('Important Notice')}</p>
<p className='text-base-content/80 text-sm'>

View file

@ -158,7 +158,7 @@ const NowPlayingBar = ({ isSelectMode }: NowPlayingBarProps) => {
className={clsx(
'not-eink:bg-base-300 eink-bordered flex items-center gap-2 rounded-full shadow-lg',
'h-14 max-w-[calc(100vw-2rem)] min-w-[60vw] sm:min-w-0 cursor-pointer px-2',
'focus-visible:ring-primary focus-visible:ring-2 focus-visible:outline-none',
'focus-visible:ring-primary focus-visible:ring-2 focus-visible:outline-hidden',
)}
>
{coverImageUrl ? (
@ -175,7 +175,7 @@ const NowPlayingBar = ({ isSelectMode }: NowPlayingBarProps) => {
)}
<button
type='button'
className='touch-target shrink-0 p-1 focus-visible:ring-primary focus-visible:ring-2 focus-visible:outline-none'
className='touch-target shrink-0 p-1 focus-visible:ring-primary focus-visible:ring-2 focus-visible:outline-hidden'
aria-label={isPlaying ? _('Pause') : _('Play')}
onClick={(e) => {
e.stopPropagation();
@ -186,7 +186,7 @@ const NowPlayingBar = ({ isSelectMode }: NowPlayingBarProps) => {
</button>
<button
type='button'
className='touch-target shrink-0 p-1 focus-visible:ring-primary focus-visible:ring-2 focus-visible:outline-none'
className='touch-target shrink-0 p-1 focus-visible:ring-primary focus-visible:ring-2 focus-visible:outline-hidden'
aria-label={_('Stop reading aloud')}
onClick={(e) => {
e.stopPropagation();

View file

@ -16,8 +16,8 @@ export function CatalogDialog({ onClose }: CatalogDialogProps) {
isOpen={true}
title={appService?.isOnlineCatalogsAccessible ? _('Online Library') : _('OPDS Catalogs')}
onClose={onClose}
bgClassName={'sm:!bg-black/75'}
boxClassName='sm:min-w-[520px] sm:w-3/4 sm:h-[85%] sm:!max-w-screen-sm'
bgClassName={'sm:bg-black/75!'}
boxClassName='sm:min-w-[520px] sm:w-3/4 sm:h-[85%] sm:max-w-(--breakpoint-sm)!'
>
<div className={clsx('bg-base-100 relative flex flex-col overflow-y-auto pb-4')}>
<CatalogManager />

View file

@ -262,7 +262,7 @@ const RecentShelf: React.FC<RecentShelfProps> = ({
aria-label={_('Scroll left')}
onClick={() => scrollByPage(-1)}
style={{ top: coverCenter ?? '50%' }}
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute start-2 -translate-y-1/2 rounded-full border p-1 shadow-sm transition-colors duration-200'
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute start-2 -translate-y-1/2 rounded-full border p-1 shadow-xs transition-colors duration-200'
>
<MdChevronLeft
size={20}
@ -276,7 +276,7 @@ const RecentShelf: React.FC<RecentShelfProps> = ({
aria-label={_('Scroll right')}
onClick={() => scrollByPage(1)}
style={{ top: coverCenter ?? '50%' }}
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute end-2 -translate-y-1/2 rounded-full border p-1 shadow-sm transition-colors duration-200'
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute end-2 -translate-y-1/2 rounded-full border p-1 shadow-xs transition-colors duration-200'
>
<MdChevronRight
size={20}

View file

@ -60,7 +60,7 @@ const SetStatusAlert: React.FC<SetStatusAlertProps> = ({
className={clsx(
'flex w-auto max-w-[90vw] flex-col gap-3',
'border-base-content/10 bg-base-200/95 rounded-2xl border p-4',
'shadow-lg backdrop-blur-sm',
'shadow-lg backdrop-blur-xs',
)}
>
{/* Header with close button for small screens */}
@ -86,7 +86,7 @@ const SetStatusAlert: React.FC<SetStatusAlertProps> = ({
key={label}
className={clsx(
'flex items-center gap-2 rounded-full border px-4 py-2',
'shadow-sm transition-all duration-200 ease-out active:scale-[0.97]',
'shadow-xs transition-all duration-200 ease-out active:scale-[0.97]',
className,
)}
onClick={() => onUpdateStatus(status)}
@ -97,7 +97,7 @@ const SetStatusAlert: React.FC<SetStatusAlertProps> = ({
<button
className={clsx(
'hidden items-center gap-2 rounded-full border px-4 py-2',
'not-eink:bg-base-300 not-eink:text-base-content not-eink:border-base-content/10 not-eink:shadow-sm',
'not-eink:bg-base-300 not-eink:text-base-content not-eink:border-base-content/10 not-eink:shadow-xs',
'eink-bordered',
'transition-all duration-200 ease-out active:scale-[0.97]',
'sm:flex',

View file

@ -323,7 +323,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
? _('Logged in as {{userDisplayName}}', { userDisplayName })
: _('Logged in')
}
labelClass='!max-w-40'
labelClass='max-w-40!'
aria-label={_('View account details and quota')}
Icon={
avatarUrl ? (
@ -352,7 +352,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ onPullLibrary, setIsDropdow
<MenuItem
label={syncRowLabel}
Icon={user ? MdSync : MdSyncProblem}
labelClass='ps-2 pe-1 !mx-0'
labelClass='ps-2 pe-1 mx-0!'
iconClassName={(user && isSyncing) || providerSyncing ? 'animate-reverse-spin' : ''}
onClick={handleSyncLibrary}
description={

View file

@ -241,7 +241,7 @@ const ShareBookDialog: React.FC<ShareBookDialogProps> = ({ isOpen, book, cfi, on
title={_('Share Book')}
onClose={onClose}
boxClassName='sm:min-w-[460px] sm:max-w-[460px] sm:h-auto sm:max-h-[90%]'
contentClassName='!px-6 !py-4'
contentClassName='px-6! py-4!'
>
<div className='flex flex-col gap-5 pt-2'>
{/* Hero: cover + metadata. Cover gets a real shadow so it reads as a
@ -355,7 +355,7 @@ const ShareBookDialog: React.FC<ShareBookDialogProps> = ({ isOpen, book, cfi, on
readOnly
value={created.url}
aria-label={_('Share URL')}
className='text-base-content min-w-0 flex-1 bg-transparent font-mono text-xs outline-none'
className='text-base-content min-w-0 flex-1 bg-transparent font-mono text-xs outline-hidden'
onFocus={(e) => e.currentTarget.select()}
/>
<button

View file

@ -195,7 +195,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<MenuItem
key={option.value}
label={option.label}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={viewMode === option.value}
onClick={() => handleSetViewMode(option.value as LibraryViewModeType)}
transient
@ -206,19 +206,19 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem
label={_('Columns')}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
labelClass='text-sm sm:text-xs'
disabled
/>
<MenuItem
label={_('Auto')}
buttonClass='min-h-10 !py-2'
buttonClass='min-h-10 py-2!'
toggled={autoColumns}
disabled={viewMode === 'list'}
siblings={
<NumberInput
className='!h-10 !p-0 !pe-1 !ps-0'
inputClassName={`!p-0 text-center text-base sm:text-sm !w-10 !h-6 !pe-0 ${autoColumns ? 'opacity-50' : ''}`}
className='h-10! p-0! pe-1! ps-0!'
inputClassName={`p-0! text-center text-base sm:text-sm w-10! h-6! pe-0! ${autoColumns ? 'opacity-50' : ''}`}
label={''}
value={columns}
disabled={viewMode === 'list'}
@ -234,7 +234,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem
label={_('Book Covers')}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
labelClass='text-sm sm:text-xs'
disabled
/>
@ -242,7 +242,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<MenuItem
key={option.value}
label={option.label}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={coverFit === option.value}
onClick={() => handleToggleCropCovers(option.value as LibraryCoverFitType)}
transient
@ -253,7 +253,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem
label={_('Hide covers')}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={settings.libraryHideCovers}
onClick={handleToggleHideCovers}
transient
@ -263,7 +263,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem
label={_('Show recently read')}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={settings.libraryRecentShelfEnabled}
onClick={handleToggleRecentShelf}
transient
@ -271,13 +271,13 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
{/* Group By - Collapsible */}
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem label={_('Group by...')} detailsOpen={true} buttonClass='py-[4px]'>
<MenuItem label={_('Group by...')} detailsOpen={true} buttonClass='py-1!'>
<ul className='ms-0 flex flex-col ps-0 before:hidden'>
{groupByOptions.map((option) => (
<MenuItem
key={option.value}
label={option.label}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={groupBy === option.value}
onClick={() => handleSetGroupBy(option.value as LibraryGroupByType)}
transient
@ -288,7 +288,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
{/* Sort By - Collapsible */}
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem label={_('Sort by...')} detailsOpen={false} buttonClass='py-[4px]'>
<MenuItem label={_('Sort by...')} detailsOpen={false} buttonClass='py-1!'>
<ul className='ms-0 flex flex-col ps-0 before:hidden'>
{sortByOptions.map((option) => {
const isImplicit = primaryIsImplicit && option.value === primaryEffective;
@ -297,7 +297,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<MenuItem
key={option.value}
label={isImplicit ? `${option.label} (${_('Auto')})` : option.label}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={toggled}
onClick={() => handleSetSortBy(option.value as LibrarySortByType)}
transient
@ -309,7 +309,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<MenuItem
key={option.value.toString()}
label={option.label}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={isAscending === option.value}
onClick={() => handleSetSortAscending(option.value)}
transient
@ -320,7 +320,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
{/* Then by - secondary sort, collapsible */}
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem label={_('Then by...')} detailsOpen={false} buttonClass='py-[4px]'>
<MenuItem label={_('Then by...')} detailsOpen={false} buttonClass='py-1!'>
<ul className='ms-0 flex flex-col ps-0 before:hidden'>
{thenSortByOptions.map((option) => {
const isImplicit = secondaryIsImplicit && option.value === secondaryEffective;
@ -329,7 +329,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<MenuItem
key={option.value}
label={isImplicit ? `${option.label} (${_('Auto')})` : option.label}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={isExplicit || isImplicit}
onClick={() => handleSetThenSortBy(option.value)}
transient
@ -343,7 +343,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
<MenuItem
key={option.value.toString()}
label={option.label}
buttonClass='min-h-8 !py-1'
buttonClass='min-h-8 py-1!'
toggled={isThenAscending === option.value}
onClick={() => handleSetThenSortAscending(option.value)}
transient

View file

@ -94,7 +94,7 @@ const WebSourcesDialog: React.FC<WebSourcesDialogProps> = ({ isOpen, onClose })
isOpen={isOpen}
onClose={onClose}
title={_('From Web Browser')}
boxClassName='sm:!w-[480px] sm:!max-w-[480px] sm:!h-auto sm:!max-h-[80vh]'
boxClassName='sm:w-[480px]! sm:max-w-[480px]! sm:h-auto! sm:max-h-[80vh]!'
>
<div className='flex flex-col gap-4 pb-6 pt-2'>
<p className='text-base-content/60 text-sm leading-relaxed'>
@ -113,7 +113,7 @@ const WebSourcesDialog: React.FC<WebSourcesDialogProps> = ({ isOpen, onClose })
onClick={() => void handleOpen(source)}
className={clsx(
'flex min-w-0 flex-1 items-center gap-3 py-3 text-start',
'focus-visible:ring-base-content/15 rounded-md focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 rounded-md focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<MdLanguage className='text-base-content/70 h-5 w-5 shrink-0' />
@ -139,14 +139,14 @@ const WebSourcesDialog: React.FC<WebSourcesDialogProps> = ({ isOpen, onClose })
<div className='flex flex-col gap-2'>
<input
type='text'
className='input input-bordered eink-bordered placeholder:text-base-content/35 w-full'
className='input eink-bordered placeholder:text-base-content/35 w-full'
placeholder={_('Name (optional)')}
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type='url'
className='input input-bordered eink-bordered placeholder:text-base-content/35 w-full'
className='input eink-bordered placeholder:text-base-content/35 w-full'
placeholder='https://calibre.example.com'
value={url}
onChange={(e) => setUrl(e.target.value)}

View file

@ -59,7 +59,7 @@ const AddFeedModal: React.FC<AddFeedModalProps> = ({ isOpen, onClose, onSubmit }
isOpen={isOpen}
onClose={onClose}
title={_('Add Feed')}
boxClassName='sm:!w-[480px] sm:!max-w-[480px] sm:!h-auto sm:!max-h-[80vh]'
boxClassName='sm:w-[480px]! sm:max-w-[480px]! sm:h-auto! sm:max-h-[80vh]!'
>
<div className='flex flex-col gap-4 pb-6 pt-2'>
<p className='text-base-content/60 text-sm leading-relaxed'>
@ -68,7 +68,7 @@ const AddFeedModal: React.FC<AddFeedModalProps> = ({ isOpen, onClose, onSubmit }
<input
type='url'
autoFocus
className='input input-bordered eink-bordered placeholder:text-base-content/35 w-full'
className='input eink-bordered placeholder:text-base-content/35 w-full'
placeholder='https://example.com/feed.xml'
value={url}
disabled={submitting}

View file

@ -112,8 +112,8 @@ export function FeedsView({ onClose }: FeedsViewProps) {
isOpen={true}
title={liveFeed ? liveFeed.title : _('Feeds')}
onClose={onClose}
bgClassName='sm:!bg-black/75'
boxClassName='sm:min-w-[520px] sm:w-3/4 sm:h-[85%] sm:!max-w-screen-sm'
bgClassName='sm:bg-black/75!'
boxClassName='sm:min-w-[520px] sm:w-3/4 sm:h-[85%] sm:max-w-(--breakpoint-sm)!'
>
<div className='bg-base-100 relative flex flex-col overflow-y-auto pb-4'>
{liveFeed ? (
@ -194,7 +194,7 @@ export function FeedsView({ onClose }: FeedsViewProps) {
<div className='flex items-center gap-2 px-4 py-3'>
<button
type='button'
className='hover:bg-base-200 flex min-w-0 flex-1 cursor-pointer flex-col gap-0.5 rounded text-start transition-colors'
className='hover:bg-base-200 flex min-w-0 flex-1 cursor-pointer flex-col gap-0.5 rounded-sm text-start transition-colors'
onClick={() => setSelectedFeed(feed)}
>
<div className='flex items-center gap-2'>

View file

@ -1990,7 +1990,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
key={term}
type='button'
onClick={() => handleSearchQueryApply(term)}
className='bg-base-300/45 hover:bg-base-300/70 text-base-content/70 max-w-[60%] flex-shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
className='bg-base-300/45 hover:bg-base-300/70 text-base-content/70 max-w-[60%] shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
>
<p className='truncate'>{term}</p>
</button>
@ -2019,7 +2019,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
<div className='flex flex-wrap items-center gap-y-1 px-4 text-base'>
<button
onClick={() => handleNavigateToPath(undefined)}
className='hover:bg-base-300 text-base-content/85 rounded px-2 py-1'
className='hover:bg-base-300 text-base-content/85 rounded-sm px-2 py-1'
>
{_('All')}
</button>
@ -2029,11 +2029,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
<React.Fragment key={index}>
<MdChevronRight size={iconSize} className='text-neutral-content' />
{isLast ? (
<span className='truncate rounded px-2 py-1'>{crumb.name}</span>
<span className='truncate rounded-sm px-2 py-1'>{crumb.name}</span>
) : (
<button
onClick={() => handleNavigateToPath(crumb.path)}
className='hover:bg-base-300 text-base-content/85 truncate rounded px-2 py-1'
className='hover:bg-base-300 text-base-content/85 truncate rounded-sm px-2 py-1'
>
{crumb.name}
</button>
@ -2052,11 +2052,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
)}
{showBookshelf &&
(libraryBooks.some((book) => !book.deletedAt) ? (
<div aria-label={_('Your Bookshelf')} className='flex min-h-0 flex-grow flex-col'>
<div aria-label={_('Your Bookshelf')} className='flex min-h-0 grow flex-col'>
<div
ref={containerRef}
className={clsx(
'scroll-container drop-zone flex min-h-0 flex-grow flex-col',
'scroll-container drop-zone flex min-h-0 grow flex-col',
isDragging && 'drag-over',
)}
style={{

View file

@ -182,7 +182,7 @@ function CatalogCard({
}}
className={clsx(
'card eink-bordered bg-base-100 border-base-200 group/card flex flex-col border transition-colors duration-150',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
isDragging && 'z-10 shadow-md',
catalog.disabled ? 'cursor-not-allowed opacity-60' : 'hover:bg-base-300 cursor-pointer',
)}
@ -197,7 +197,7 @@ function CatalogCard({
// onKeyDown) keeps handle clicks and Space/arrow keys from also
// browsing the catalog.
<div
className='-ms-1.5 -mt-1 flex-shrink-0'
className='-ms-1.5 -mt-1 shrink-0'
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
@ -207,7 +207,7 @@ function CatalogCard({
over the catalog name and swallow taps meant to browse. */}
<button
type='button'
className='text-base-content/45 hover:bg-base-200 hover:text-base-content focus-visible:ring-base-content/15 flex h-7 w-7 cursor-grab touch-none items-center justify-center rounded-md transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 active:cursor-grabbing'
className='text-base-content/45 hover:bg-base-200 hover:text-base-content focus-visible:ring-base-content/15 flex h-7 w-7 cursor-grab touch-none items-center justify-center rounded-md transition-colors duration-150 focus-visible:outline-hidden focus-visible:ring-2 active:cursor-grabbing'
{...attributes}
{...listeners}
aria-label={_('Drag to reorder')}
@ -218,7 +218,7 @@ function CatalogCard({
</div>
)}
<h4 className='flex min-w-0 flex-1 items-center gap-1.5 text-sm font-semibold'>
{catalog.icon && <span className='flex-shrink-0'>{catalog.icon}</span>}
{catalog.icon && <span className='shrink-0'>{catalog.icon}</span>}
<span className='truncate'>{catalog.name}</span>
</h4>
{/* stopPropagation on the trigger wrapper so opening
@ -228,14 +228,14 @@ function CatalogCard({
position:absolute rule — don't add !relative here
or the menu inlines into the card layout. */}
<div
className='-me-1.5 -mt-1 flex-shrink-0'
className='-me-1.5 -mt-1 shrink-0'
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<Dropdown
label={_('Catalog actions')}
className='dropdown-bottom dropdown-end'
buttonClassName='text-base-content/55 hover:bg-base-200 hover:text-base-content focus-visible:ring-base-content/15 flex h-7 w-7 items-center justify-center rounded-md transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2'
buttonClassName='text-base-content/55 hover:bg-base-200 hover:text-base-content focus-visible:ring-base-content/15 flex h-7 w-7 items-center justify-center rounded-md transition-colors duration-150 focus-visible:outline-hidden focus-visible:ring-2'
toggleButton={<IoEllipsisVertical className='h-4 w-4' />}
>
<Menu className='dropdown-content no-triangle border-base-300 z-20 mt-1 min-w-[8rem] rounded-lg border shadow-lg'>
@ -288,7 +288,7 @@ function CatalogCard({
</span>
<input
type='checkbox'
className='toggle toggle-sm toggle-primary flex-shrink-0'
className='toggle toggle-sm toggle-primary shrink-0'
checked={!!catalog.autoDownload}
disabled={!!catalog.disabled}
onChange={() => onRequestToggleAutoDownload(catalog.id)}
@ -674,7 +674,7 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
<SectionTitle>{_('My Catalogs')}</SectionTitle>
<button
onClick={() => setShowAddDialog(true)}
className='eink-bordered border-base-200 hover:border-base-300 hover:bg-base-200/60 focus-visible:ring-base-content/15 inline-flex h-8 items-center gap-1.5 rounded-lg border px-3 text-sm font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2'
className='eink-bordered border-base-200 hover:border-base-300 hover:bg-base-200/60 focus-visible:ring-base-content/15 inline-flex h-8 items-center gap-1.5 rounded-lg border px-3 text-sm font-medium transition-colors duration-150 focus-visible:outline-hidden focus-visible:ring-2'
>
<IoAdd className='h-4 w-4' />
{_('Add Catalog')}
@ -735,9 +735,9 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
<button
type='button'
onClick={() => handleOpenCatalog(catalog)}
className='flex w-full min-w-0 items-center gap-1.5 rounded-sm text-start text-sm font-semibold transition-colors duration-150 hover:underline focus-visible:underline focus-visible:outline-none'
className='flex w-full min-w-0 items-center gap-1.5 rounded-xs text-start text-sm font-semibold transition-colors duration-150 hover:underline focus-visible:underline focus-visible:outline-hidden'
>
{catalog.icon && <span className='flex-shrink-0'>{catalog.icon}</span>}
{catalog.icon && <span className='shrink-0'>{catalog.icon}</span>}
<span className='truncate'>{catalog.name}</span>
</button>
</h4>
@ -749,14 +749,14 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
<div className='border-base-200 mt-auto flex items-center justify-end gap-1 border-t pt-3'>
<button
onClick={() => handleAddPopularCatalog(catalog)}
className='hover:bg-base-200 focus-visible:ring-base-content/15 inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2'
className='hover:bg-base-200 focus-visible:ring-base-content/15 inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors duration-150 focus-visible:outline-hidden focus-visible:ring-2'
>
<IoAdd className='h-4 w-4' />
{_('Add')}
</button>
<button
onClick={() => handleOpenCatalog(catalog)}
className='hover:bg-base-200 focus-visible:ring-base-content/15 inline-flex items-center gap-0.5 rounded-md px-2 py-1 text-xs font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2'
className='hover:bg-base-200 focus-visible:ring-base-content/15 inline-flex items-center gap-0.5 rounded-md px-2 py-1 text-xs font-medium transition-colors duration-150 focus-visible:outline-hidden focus-visible:ring-2'
>
{_('Browse')}
<MdChevronRight className='h-4 w-4' />
@ -783,24 +783,24 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
}}
className='space-y-4'
>
<div className='form-control'>
<div className='label'>
<span className='label-text'>{_('Catalog Name')} *</span>
<div className='flex flex-col'>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm'>{_('Catalog Name')} *</span>
</div>
<input
type='text'
value={newCatalog.name}
onChange={(e) => setNewCatalog({ ...newCatalog, name: e.target.value })}
placeholder={_('My Calibre Library')}
className='input input-bordered eink-bordered placeholder:text-sm'
className='input eink-bordered placeholder:text-sm'
disabled={isValidating}
required
/>
</div>
<div className='form-control'>
<div className='label'>
<span className='label-text'>{_('OPDS URL')} *</span>
<div className='flex flex-col'>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm'>{_('OPDS URL')} *</span>
</div>
<input
type='url'
@ -810,20 +810,20 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
setUrlError('');
}}
placeholder='https://example.com/opds'
className='input input-bordered eink-bordered placeholder:text-sm'
className='input eink-bordered placeholder:text-sm'
disabled={isValidating}
required
/>
{urlError && (
<div className='label'>
<span className='label-text-alt text-error'>{urlError}</span>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-xs text-error'>{urlError}</span>
</div>
)}
</div>
<div className='form-control'>
<div className='label'>
<span className='label-text'>{_('Username (optional)')}</span>
<div className='flex flex-col'>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm'>{_('Username (optional)')}</span>
</div>
<input
type='text'
@ -833,15 +833,15 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
setProxyConsentError('');
}}
placeholder={_('Username')}
className='input input-bordered eink-bordered placeholder:text-sm'
className='input eink-bordered placeholder:text-sm'
disabled={isValidating}
autoComplete='username'
/>
</div>
<div className='form-control'>
<div className='label'>
<span className='label-text'>{_('Password (optional)')}</span>
<div className='flex flex-col'>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm'>{_('Password (optional)')}</span>
</div>
<div className='relative'>
<input
@ -852,7 +852,7 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
setProxyConsentError('');
}}
placeholder={_('Password')}
className='input input-bordered eink-bordered w-full pr-10 placeholder:text-sm'
className='input eink-bordered w-full pr-10 placeholder:text-sm'
disabled={isValidating}
autoComplete='current-password'
/>
@ -871,9 +871,9 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
</div>
</div>
<div className='form-control'>
<div className='label'>
<span className='label-text'>{_('Custom Headers (optional)')}</span>
<div className='flex flex-col'>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm'>{_('Custom Headers (optional)')}</span>
</div>
<textarea
value={newCatalog.customHeadersInput}
@ -886,26 +886,26 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
'CF-Access-Client-Id': 'your-client-id',
'CF-Access-Client-Secret': 'your-client-secret',
})}
className='textarea textarea-bordered eink-bordered font-mono text-sm placeholder:text-xs'
className='textarea eink-bordered font-mono text-sm placeholder:text-xs'
rows={4}
disabled={isValidating}
spellCheck={false}
/>
<div className='label'>
<span className='label-text-alt text-base-content/60'>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-xs text-base-content/60'>
{_('Add one header per line using "Header-Name: value".')}
</span>
</div>
{headerError && (
<div className='label pt-0'>
<span className='label-text-alt text-error'>{headerError}</span>
<div className='flex select-none items-center justify-between px-1 pb-2 pt-0'>
<span className='text-xs text-error'>{headerError}</span>
</div>
)}
</div>
{isWebCatalogProxyWarningRequired && (
<div className='form-control border-warning/30 bg-warning/10 rounded-lg border p-4'>
<label className='label cursor-pointer items-start justify-start gap-3 p-0'>
<div className='flex flex-col border-warning/30 bg-warning/10 rounded-lg border p-4'>
<label className='flex cursor-pointer select-none items-start justify-start gap-3 p-0'>
<input
type='checkbox'
className='checkbox checkbox-sm mt-0.5'
@ -916,36 +916,36 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
}}
disabled={isValidating}
/>
<span className='label-text text-sm leading-6'>
<span className='text-sm text-sm leading-6'>
{_(
'I understand this OPDS connection will be proxied through Readest servers on the web app. If I do not trust Readest with these credentials or headers, I should use the native app instead.',
)}
</span>
</label>
{proxyConsentError && (
<div className='label px-0 pb-0 pt-2'>
<span className='label-text-alt text-error'>{proxyConsentError}</span>
<div className='flex select-none items-center justify-between px-0 pb-0 pt-2'>
<span className='text-xs text-error'>{proxyConsentError}</span>
</div>
)}
</div>
)}
<div className='form-control'>
<div className='label'>
<span className='label-text'>{_('Description (optional)')}</span>
<div className='flex flex-col'>
<div className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm'>{_('Description (optional)')}</span>
</div>
<textarea
value={newCatalog.description}
onChange={(e) => setNewCatalog({ ...newCatalog, description: e.target.value })}
placeholder={_('A brief description of this catalog')}
className='textarea textarea-bordered eink-bordered text-sm placeholder:text-sm'
className='textarea eink-bordered text-sm placeholder:text-sm'
rows={2}
disabled={isValidating}
/>
</div>
<div className='form-control'>
<label className='label cursor-pointer justify-start gap-3 p-0'>
<div className='flex flex-col'>
<label className='flex cursor-pointer select-none items-center justify-start gap-3 p-0'>
<input
type='checkbox'
className='toggle toggle-sm toggle-primary'
@ -956,7 +956,7 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
disabled={isValidating}
/>
<div>
<span className='label-text'>{_('Auto-download new items')}</span>
<span className='text-sm'>{_('Auto-download new items')}</span>
<p className='text-base-content/60 text-xs'>
{_('Automatically download new publications when the app syncs')}
</p>
@ -974,7 +974,7 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
'h-10 rounded-lg px-4 text-sm font-medium',
'text-base-content hover:bg-base-200',
'transition-colors duration-150',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
'disabled:cursor-not-allowed disabled:opacity-60',
'disabled:hover:bg-transparent',
)}
@ -985,9 +985,9 @@ export function CatalogManager({ inSubPage = false }: CatalogManagerProps = {})
type='submit'
disabled={isValidating}
className={clsx(
'btn btn-primary',
'btn btn-contrast',
'h-10 min-h-10 rounded-lg border-0 px-5 text-sm font-medium',
'focus-visible:ring-primary/40 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/40 focus-visible:outline-hidden focus-visible:ring-2',
isValidating && 'opacity-60',
)}
>

View file

@ -116,7 +116,7 @@ export function FailedDownloadsDialog({ catalogId, catalogName, onClose }: Props
{failed.map((entry) => (
<li
key={entry.entryId}
className='border-base-300 flex items-start justify-between gap-3 rounded border p-3'
className='border-base-300 flex items-start justify-between gap-3 rounded-sm border p-3'
>
<div className='min-w-0 flex-1'>
<p className='truncate text-sm font-medium'>{entry.title}</p>
@ -127,7 +127,7 @@ export function FailedDownloadsDialog({ catalogId, catalogName, onClose }: Props
{dayjs(entry.lastAttemptAt).fromNow()}
</p>
</div>
<div className='flex flex-shrink-0 flex-col gap-1'>
<div className='flex shrink-0 flex-col gap-1'>
<button
type='button'
onClick={() => retryEntry(entry)}

View file

@ -76,7 +76,7 @@ export function FeedView({
return (
<div className='flex h-full flex-col'>
{/* Header */}
<div className='opds-header flex-shrink-0 px-4 py-6'>
<div className='opds-header shrink-0 px-4 py-6'>
{feed.metadata?.title && <h1 className='mb-2 text-xl font-bold'>{feed.metadata.title}</h1>}
{feed.metadata?.subtitle && (
<p className='text-base-content/70 text-sm'>{feed.metadata.subtitle}</p>
@ -86,7 +86,7 @@ export function FeedView({
<div className='flex min-h-0 flex-1 gap-6'>
{/* Facets Sidebar */}
{hasFacets && (
<aside className='hidden w-64 flex-shrink-0 overflow-y-auto lg:block'>
<aside className='hidden w-64 shrink-0 overflow-y-auto lg:block'>
<div className='px-4'>
{onAddCatalog && (
<div className='mb-4'>
@ -119,7 +119,7 @@ export function FeedView({
<li key={linkIndex}>
<button
onClick={() => handleNavigationClick(href)}
className={`w-full rounded px-3 py-1.5 text-left text-sm transition-colors ${
className={`w-full rounded-sm px-3 py-1.5 text-left text-sm transition-colors ${
isActive
? 'bg-primary text-primary-content font-medium'
: 'hover:bg-base-200'
@ -149,7 +149,7 @@ export function FeedView({
<div className='flex min-w-0 flex-1 flex-col'>
{/* Navigation Items */}
{feed.navigation && feed.navigation.length > 0 && (
<section className='opds-navigation flex-shrink-0 pb-6'>
<section className='opds-navigation shrink-0 pb-6'>
<div className={navigationClassName}>
{feed.navigation.map((item, index: number) => (
<NavigationCard
@ -178,7 +178,7 @@ export function FeedView({
{/* Groups */}
{feed.groups?.map((group, groupIndex: number) => (
<section key={groupIndex} className={`flex-shrink-0 ${useCarousel ? 'mb-6' : 'mb-12'}`}>
<section key={groupIndex} className={`shrink-0 ${useCarousel ? 'mb-6' : 'mb-12'}`}>
{group.metadata && (
<div
className={`flex items-center justify-between px-4 ${useCarousel ? 'mb-2' : 'mb-4'}`}
@ -268,7 +268,7 @@ export function FeedView({
{/* Pagination */}
{pagination.some((links) => links && links.length > 0) && (
<nav className='flex flex-shrink-0 justify-center gap-2 py-4'>
<nav className='flex shrink-0 justify-center gap-2 py-4'>
<button
onClick={() => handlePaginationClick(pagination[0])}
disabled={!pagination[0]}

View file

@ -95,7 +95,7 @@ export function GroupCarousel({
aria-label={_('Scroll left')}
onClick={() => scrollByPage(-1)}
style={{ top: coverCenter ?? '50%' }}
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute left-2 -translate-y-1/2 rounded-full border p-1 shadow-sm transition-colors duration-200'
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute left-2 -translate-y-1/2 rounded-full border p-1 shadow-xs transition-colors duration-200'
>
<MdChevronLeft size={20} className='text-base-content/60 hover:text-base-content/80' />
</button>
@ -105,7 +105,7 @@ export function GroupCarousel({
aria-label={_('Scroll right')}
onClick={() => scrollByPage(1)}
style={{ top: coverCenter ?? '50%' }}
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute right-2 -translate-y-1/2 rounded-full border p-1 shadow-sm transition-colors duration-200'
className='eink-bordered bg-base-100 border-base-content/10 hover:border-base-content/30 absolute right-2 -translate-y-1/2 rounded-full border p-1 shadow-xs transition-colors duration-200'
>
<MdChevronRight size={20} className='text-base-content/60 hover:text-base-content/80' />
</button>

View file

@ -114,7 +114,7 @@ export function Navigation({
appService?.isMobile ? '' : 'bg-base-100',
)}
>
<div className={clsx('justify-start gap-1 sm:gap-3', isTrafficLightVisible && '!pl-16')}>
<div className={clsx('justify-start gap-1 sm:gap-3', isTrafficLightVisible && 'pl-16!')}>
<div className='flex gap-1'>
{onBack && (
<button
@ -142,7 +142,7 @@ export function Navigation({
</button>
</div>
<div className='flex-grow px-3 sm:px-5'>
<div className='grow px-3 sm:px-5'>
<div className='exclude-title-bar-mousedown relative flex w-full items-center'>
<span className='text-base-content/50 absolute left-3'>
<FaSearch className='h-4 w-4' />
@ -156,13 +156,13 @@ export function Navigation({
onChange={handleSearchChange}
spellCheck='false'
className={clsx(
'input rounded-badge h-9 w-full pl-10 pr-4 sm:h-7',
'input rounded-selector h-9 w-full pl-10 pr-4 sm:h-7',
viewSettings?.isEink
? 'border-1 border-base-content focus:border-base-content'
: 'bg-base-300/45 border-none',
'font-sans text-sm font-light',
'placeholder:text-base-content/50 truncate',
'focus:outline-none focus:ring-0',
'focus:outline-hidden focus:ring-0',
)}
/>
<div className='text-base-content/50 absolute right-2 flex items-center space-x-2 sm:space-x-4'>

View file

@ -20,7 +20,7 @@ export function NavigationCard({ item, baseURL, onClick, resolveURL }: Navigatio
<div
role='none'
onClick={() => onClick(href)}
className='card bg-base-100 border-base-300 hover:bg-base-200/30 cursor-pointer rounded-lg border shadow-sm transition-shadow'
className='card bg-base-100 border-base-300 hover:bg-base-200/30 cursor-pointer rounded-lg border shadow-xs transition-shadow'
>
<div className='card-body flex justify-center p-4'>
<div className='flex items-start gap-3'>
@ -40,7 +40,7 @@ export function NavigationCard({ item, baseURL, onClick, resolveURL }: Navigatio
)}
</div>
<div className='flex h-full items-center justify-center'>
<IoChevronForward className='text-base-content/40 h-5 w-5 flex-shrink-0' />
<IoChevronForward className='text-base-content/40 h-5 w-5 shrink-0' />
</div>
</div>
</div>

View file

@ -51,7 +51,7 @@ export function PublicationCard({
return (
<div role='none' onClick={onClick} className='card cursor-pointer transition-shadow'>
<figure className='bg-base-200 relative aspect-[28/41] overflow-hidden rounded shadow-md'>
<figure className='bg-base-200 relative aspect-[28/41] overflow-hidden rounded-sm shadow-md'>
<CachedImage
src={imageUrl}
alt={publication.metadata?.title || 'Book cover'}

View file

@ -191,7 +191,7 @@ export function PublicationView({
// it opens.
className={clsx(
'dropdown-content no-triangle',
'border-base-300 eink-bordered !bg-base-200 z-20 mt-2 max-w-[80vw] min-w-max',
'border-base-300 eink-bordered bg-base-200! z-20 mt-2 max-w-[80vw] min-w-max',
'rounded-2xl border shadow-2xl',
)}
>
@ -215,7 +215,7 @@ export function PublicationView({
// has no shape to split, so its caret rendered as a detached sliver.
const SOLID_ACTION = 'btn btn-contrast';
const FLAT_ACTION =
'btn eink-bordered border-transparent !bg-base-200 hover:!bg-base-300 text-base-content';
'btn eink-bordered border-transparent bg-base-200! hover:bg-base-300! text-base-content';
/**
* A default action plus, when there is more than one format to choose from, a
@ -273,7 +273,7 @@ export function PublicationView({
return (
<div className='flex w-full flex-col px-6 py-6'>
<div className='mb-6 flex w-full flex-row items-start gap-6 max-[320px]:flex-col'>
<div className='h-44 flex-shrink-0 sm:h-56 md:h-64'>
<div className='h-44 shrink-0 sm:h-56 md:h-64'>
<div className='bg-base-200 relative aspect-[28/41] h-full overflow-hidden rounded-none shadow-lg'>
<CachedImage
src={imageUrl}

View file

@ -79,9 +79,9 @@ export function SearchView({ search, baseURL, onNavigate, resolveURL }: SearchVi
<form onSubmit={handleSubmit} className='space-y-4'>
{(search.params || []).map((param) => (
<div key={`${param.ns || 'default'}-${param.name}`} className='form-control'>
<label className='label'>
<span className='label-text font-medium'>
<div key={`${param.ns || 'default'}-${param.name}`} className='flex flex-col'>
<label className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm font-medium'>
{getParamLabel(param.name)}
{param.required && <span className='text-error ml-1'>*</span>}
</span>
@ -92,7 +92,7 @@ export function SearchView({ search, baseURL, onNavigate, resolveURL }: SearchVi
onChange={(e) => handleInputChange(param.name, e.target.value)}
required={param.required}
placeholder={`${_('Enter {{terms}}', { terms: getParamLabel(param.name).toLowerCase() })}`}
className='input input-bordered w-full'
className='input w-full'
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={
param.name === 'searchTerms' ||

View file

@ -1088,18 +1088,18 @@ export default function BrowserPage() {
title={_('Add to My Catalogs')}
onClose={() => setShowAddCatalog(false)}
boxClassName='sm:max-w-md sm:h-auto'
contentClassName='!px-6 !py-4'
contentClassName='px-6! py-4!'
>
<div className='flex flex-col gap-4 pt-2'>
<div className='form-control'>
<label className='label'>
<span className='label-text font-medium text-sm'>{_('Catalog Name')}</span>
<div className='flex flex-col'>
<label className='flex select-none items-center justify-between px-1 py-2'>
<span className='text-sm font-medium text-sm'>{_('Catalog Name')}</span>
</label>
<input
type='text'
value={newCatalogName}
onChange={(e) => setNewCatalogName(e.target.value)}
className='input input-bordered eink-bordered w-full'
className='input eink-bordered w-full'
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && newCatalogName.trim()) {

View file

@ -108,7 +108,7 @@ const AutoScrollControl: React.FC<AutoScrollControlProps> = ({
'transition-[opacity,filter,transform] duration-200 ease-out',
isVisible
? 'pointer-events-auto translate-y-0 scale-100 opacity-100 blur-0'
: 'pointer-events-none translate-y-4 scale-90 opacity-0 blur-sm',
: 'pointer-events-none translate-y-4 scale-90 opacity-0 blur-xs',
)}
style={{
paddingBottom: appService?.hasSafeAreaInset ? `${gridInsets.bottom * 0.33}px` : 0,
@ -128,7 +128,7 @@ const AutoScrollControl: React.FC<AutoScrollControlProps> = ({
'text-base-content flex items-center gap-1 rounded-full px-3 py-1.5',
'not-eink:bg-base-300 eink-bordered',
'not-eink:border-base-content/10 not-eink:border',
'shadow-sm backdrop-blur-md',
'shadow-xs backdrop-blur-md',
)}
>
<button

View file

@ -360,7 +360,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook, onGoToLibr
return (
<div
className={clsx('books-grid bg-base-100 relative grid h-full flex-grow')}
className={clsx('books-grid bg-base-100 relative grid h-full grow')}
style={gridStyle}
role='main'
aria-label={_('Books Content')}

View file

@ -1086,7 +1086,7 @@ const FoliateViewer: React.FC<{
role='main'
aria-label={_('Book Content')}
className={clsx(
'foliate-viewer absolute h-[100%] w-[100%] focus:outline-none',
'foliate-viewer absolute h-[100%] w-[100%] focus:outline-hidden',
viewState?.loading && 'bg-base-100',
)}
style={{

View file

@ -619,7 +619,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
onClick={handleBack}
className={clsx(
'btn btn-ghost btn-circle eink-bordered text-base-content bg-base-200/80 hover:bg-base-200',
'z-10 h-8 min-h-8 w-8 p-0 shadow-sm',
'z-10 h-8 min-h-8 w-8 p-0 shadow-xs',
)}
>
<MdArrowBack size={size18} />

View file

@ -261,7 +261,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
: _('Enable Quick Action on Selection')
}
className='exclude-title-bar-mousedown dropdown-bottom dropdown-center'
menuClassName='!relative'
menuClassName='relative!'
buttonClassName={clsx(
'btn btn-ghost h-8 min-h-8 w-8 p-0',
viewSettings?.annotationQuickAction && 'bg-base-300/50',
@ -296,7 +296,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
className={clsx(
'header-title z-15 bg-base-100 pointer-events-none hidden flex-1 items-center justify-center sm:flex',
!windowButtonVisible && 'absolute inset-0',
isHeaderCompact && '!hidden',
isHeaderCompact && 'hidden!',
)}
{...getBookDataAttributes(bookTitle, bookData?.book?.metadata)}
>

View file

@ -571,7 +571,7 @@ const ImageViewer: React.FC<ImageViewerProps> = ({
tabIndex={-1}
role='button'
aria-label={_('Image viewer')}
className='no-context-menu fixed inset-0 z-50 flex items-center justify-center outline-none'
className='no-context-menu fixed inset-0 z-50 flex items-center justify-center outline-hidden'
onKeyDown={handleKeyDown}
onWheel={handleWheel}
onTouchMove={onTouchMove}

View file

@ -42,7 +42,7 @@ const KOSyncConflictResolver: React.FC<KOSyncConflictResolverProps> = ({
'transition-colors duration-150',
'hover:border-base-300 hover:bg-base-200/60',
'active:bg-base-200/80',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<div className='flex min-w-0 flex-1 flex-col gap-1'>
@ -50,7 +50,7 @@ const KOSyncConflictResolver: React.FC<KOSyncConflictResolverProps> = ({
`as='span'` because this lives inside a button, not as a
document heading; opacity override expresses the
"secondary on this surface" relationship. */}
<SectionTitle as='span' className='!text-base-content/55 !ps-0'>
<SectionTitle as='span' className='text-base-content/55! ps-0!'>
{_('This device')}
</SectionTitle>
<span className='line-clamp-2 text-sm font-medium leading-snug'>
@ -65,14 +65,14 @@ const KOSyncConflictResolver: React.FC<KOSyncConflictResolverProps> = ({
'btn btn-primary group',
'h-auto min-h-0 w-full justify-start gap-3',
'rounded-xl border-0 px-4 py-3.5 text-left font-normal normal-case',
'focus-visible:ring-primary/40 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-primary/40 focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<div className='flex min-w-0 flex-1 flex-col items-start gap-1'>
{/* On the primary-color button background the default
/65 token would clash; opacity-75 inherits the button's
contrast color and dims it uniformly. */}
<SectionTitle as='span' className='!ps-0 !text-current opacity-75'>
<SectionTitle as='span' className='ps-0! text-current! opacity-75'>
{remoteDeviceName}
</SectionTitle>
<span className='line-clamp-2 text-sm font-medium leading-snug'>

View file

@ -97,7 +97,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<button
onClick={handleGoLeftSection}
className={clsx(
'flex h-20 w-20 items-center justify-center focus:outline-none',
'flex h-20 w-20 items-center justify-center focus:outline-hidden',
!isPageNavigationButtonsVisible && appService?.isAndroidApp && 'h-4 w-4',
)}
aria-hidden={false}
@ -107,7 +107,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<span
className={clsx(
'flex h-12 w-12 items-center justify-center rounded-full',
'bg-base-100/90 shadow-lg backdrop-blur-sm',
'bg-base-100/90 shadow-lg backdrop-blur-xs',
'eink:border eink:border-base-content not-eink:group-hover:bg-base-200',
'transition-transform active:scale-95',
)}
@ -118,7 +118,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<button
onClick={handleGoLeftPage}
className={clsx(
'flex h-20 w-20 items-center justify-center focus:outline-none',
'flex h-20 w-20 items-center justify-center focus:outline-hidden',
!isPageNavigationButtonsVisible && appService?.isAndroidApp && 'h-4 w-4',
)}
aria-hidden={false}
@ -128,7 +128,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<span
className={clsx(
'flex h-12 w-12 items-center justify-center rounded-full',
'bg-base-100/90 shadow-lg backdrop-blur-sm',
'bg-base-100/90 shadow-lg backdrop-blur-xs',
'eink:border eink:border-base-content not-eink:group-hover:bg-base-200',
'transition-transform active:scale-95',
)}
@ -149,7 +149,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<button
onClick={handleGoRightPage}
className={clsx(
'flex h-20 w-20 items-center justify-center focus:outline-none',
'flex h-20 w-20 items-center justify-center focus:outline-hidden',
!isPageNavigationButtonsVisible && appService?.isAndroidApp && 'h-4 w-4',
)}
aria-hidden={false}
@ -159,7 +159,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<span
className={clsx(
'flex h-12 w-12 items-center justify-center rounded-full',
'bg-base-100/90 shadow-lg backdrop-blur-sm',
'bg-base-100/90 shadow-lg backdrop-blur-xs',
'eink:border eink:border-base-content not-eink:group-hover:bg-base-200',
'transition-transform active:scale-95',
)}
@ -170,7 +170,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<button
onClick={handleGoRightSection}
className={clsx(
'flex h-20 w-20 items-center justify-center focus:outline-none',
'flex h-20 w-20 items-center justify-center focus:outline-hidden',
!isPageNavigationButtonsVisible && appService?.isAndroidApp && 'h-4 w-4',
)}
aria-hidden={false}
@ -180,7 +180,7 @@ const PageNavigationButtons: React.FC<PageNavigationButtonsProps> = ({
<span
className={clsx(
'flex h-12 w-12 items-center justify-center rounded-full',
'bg-base-100/90 shadow-lg backdrop-blur-sm',
'bg-base-100/90 shadow-lg backdrop-blur-xs',
'eink:border eink:border-base-content not-eink:group-hover:bg-base-200',
'transition-transform active:scale-95',
)}

View file

@ -288,7 +288,7 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
<div
className={clsx(
'progress-info items-center text-end tabular-nums truncate',
'progress-readout items-center text-end tabular-nums truncate',
!stickyBarActive && 'flex-1 min-w-0',
)}
>

View file

@ -119,7 +119,10 @@ const RuleItem: React.FC<{
{isSelection ? _('Selected text:') : _('Find:')}
</label>
<input
className={clsx('input input-sm text-sm', isSelection && 'bg-base-200 opacity-60')}
className={clsx(
'input input-sm border-transparent text-sm',
isSelection && 'bg-base-200 opacity-60',
)}
value={editingData.pattern}
disabled={isSelection}
spellCheck='false'
@ -130,7 +133,7 @@ const RuleItem: React.FC<{
<div className='flex flex-col gap-1.5'>
<label className='text-base-content/70 text-xs font-medium'>{_('Replace with:')}</label>
<input
className='input input-sm text-sm'
className='input input-sm border-transparent text-sm'
value={editingData.replacement}
spellCheck='false'
onChange={(e) => onEditChange({ replacement: e.target.value })}
@ -561,12 +564,12 @@ export const ProofreadRulesManager: React.FC = () => {
title={_('Proofread Replacement Rules')}
// Cap the height on desktop (where the modal is auto-height) so the body
// scrolls; on mobile the modal is full-height and the body fills it.
boxClassName='sm:!min-w-[560px] sm:!max-w-[640px] sm:h-auto sm:!max-h-[80vh]'
boxClassName='sm:min-w-[560px]! sm:max-w-[640px]! sm:h-auto sm:max-h-[80vh]!'
// Drop the body's default horizontal padding so the scrollbar rides the
// modal's right edge (the inner `p-4 sm:p-6` keeps content off it), and
// `min-h-0` lets the flex-grow body shrink-to-scroll inside the capped
// modal instead of overflowing.
contentClassName='!px-0 min-h-0'
contentClassName='px-0! min-h-0'
>
{isOpen && (
<div className='flex flex-col gap-6 p-4 sm:p-6'>
@ -574,14 +577,14 @@ export const ProofreadRulesManager: React.FC = () => {
<SectionTitle>{_('Add Rule')}</SectionTitle>
<div className='card eink-bordered border-base-200 bg-base-100 gap-3 border p-4'>
<input
className='input input-bordered eink-bordered h-11 w-full text-sm focus:outline-none'
className='input eink-bordered h-11 w-full text-sm focus:outline-hidden'
placeholder={_('Find...')}
spellCheck='false'
value={addPattern}
onChange={(e) => setAddPattern(e.target.value)}
/>
<input
className='input input-bordered eink-bordered h-11 w-full text-sm focus:outline-none'
className='input eink-bordered h-11 w-full text-sm focus:outline-hidden'
placeholder={_('Replace with...')}
spellCheck='false'
value={addReplacement}
@ -594,7 +597,7 @@ export const ProofreadRulesManager: React.FC = () => {
<label className='flex items-center gap-2'>
<span className='text-base-content/70 text-sm'>{_('Scope:')}</span>
<select
className='select select-sm select-bordered eink-bordered min-h-9 h-9'
className='select select-sm eink-bordered min-h-9 h-9'
value={addScope}
onChange={(e) =>
setAddScope(e.target.value as Exclude<ProofreadScope, 'selection'>)

View file

@ -44,7 +44,7 @@ const SyncInfoDialog: React.FC<SyncInfoDialogProps> = ({
onClose={onClose}
snapHeight={0.7}
title={_('Sync Info')}
boxClassName='sm:!min-w-[520px] sm:h-auto'
boxClassName='sm:min-w-[520px]! sm:h-auto'
>
{isOpen && (
<div className='mb-4 mt-0 flex flex-col gap-3 p-2 sm:p-4'>

View file

@ -177,7 +177,7 @@ const TableViewer: React.FC<TableViewerProps> = ({ gridInsets, html, isDarkMode,
tabIndex={-1}
role='button'
aria-label={_('Table viewer')}
className='fixed inset-0 z-50 flex items-center justify-center outline-none'
className='fixed inset-0 z-50 flex items-center justify-center outline-hidden'
onKeyDown={handleKeyDown}
onWheel={handleWheel}
>

View file

@ -503,7 +503,7 @@ export const DictionaryResultsBody: React.FC<DictionaryResultsBodyProps> = ({
{isLoading && (
<div
data-testid='dict-card-skeleton'
className='bg-base-200/50 h-12 animate-pulse rounded'
className='bg-base-200/50 h-12 animate-pulse rounded-sm'
/>
)}
<div

View file

@ -37,7 +37,7 @@ const DictionarySheet: React.FC<DictionarySheetProps> = ({ word, lang, onDismiss
speaking={state.isSpeaking}
/>
}
contentClassName='!px-0 !mt-0'
contentClassName='px-0! mt-0!'
onClose={onDismiss}
>
<DictionaryResultsBody {...state} />

View file

@ -450,8 +450,8 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
isOpen={isOpen}
title={_('Export Annotations')}
onClose={onCancel}
boxClassName='sm:!w-[75%] sm:h-auto sm:!max-h-[90vh] sm:!max-w-5xl'
contentClassName='sm:!px-8 sm:!py-2'
boxClassName='sm:w-[75%]! sm:h-auto sm:max-h-[90vh]! sm:max-w-5xl!'
contentClassName='sm:px-8! sm:py-2!'
>
<div className='flex flex-col gap-4'>
{/* Export Format */}
@ -465,7 +465,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
exportFormat: e.target.value as NoteExportFormat,
}))
}
className='select select-bordered select-sm eink-bordered'
className='select select-sm eink-bordered'
>
<option value='markdown'>{_('Markdown')}</option>
<option value='text'>{_('Plain Text')}</option>
@ -611,7 +611,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
linkType: e.target.value as AnnotationLinkType,
}))
}
className='select select-bordered select-sm eink-bordered'
className='select select-sm eink-bordered'
>
<option value='app'>{_('App Link')}</option>
<option value='web'>{_('Web Link')}</option>
@ -721,7 +721,7 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
onChange={(e) =>
setExportConfig({ ...exportConfig, customTemplate: e.target.value })
}
className='textarea textarea-bordered w-full font-mono text-xs'
className='textarea w-full font-mono text-xs'
rows={12}
placeholder={defaultTemplate}
/>
@ -732,29 +732,29 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
<p className='mb-2 font-bold'>{_('Template Syntax:')}</p>
<ul className='space-y-1 font-mono'>
<li>
<code className='bg-base-300 rounded px-1'>{'{{ variable }}'}</code> -{' '}
{_('Insert value')}
<code className='bg-base-300 rounded-sm px-1'>{'{{ variable }}'}</code>{' '}
- {_('Insert value')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>
<code className='bg-base-300 rounded-sm px-1'>
{'{{ variable | date }}'}
</code>{' '}
- {_('Format date (locale)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>
<code className='bg-base-300 rounded-sm px-1'>
{"{{ variable | date('%Y-%m-%d') }}"}
</code>{' '}
- {_('Format date (custom)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>
<code className='bg-base-300 rounded-sm px-1'>
{'{% if variable %}...{% endif %}'}
</code>{' '}
- {_('Conditional')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>
<code className='bg-base-300 rounded-sm px-1'>
{'{% for item in list %}...{% endfor %}'}
</code>{' '}
- {_('Loop')}
@ -765,68 +765,70 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
<p className='mb-2 font-bold'>{_('Available Variables:')}</p>
<ul className='space-y-1'>
<li>
<code className='bg-base-300 rounded px-1'>title</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>title</code> -{' '}
{_('Book title')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>author</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>author</code> -{' '}
{_('Book author')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>exportDate</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>exportDate</code> -{' '}
{_('Export date')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>coverImageUrl</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>coverImageUrl</code> -{' '}
{_('Public cover image URL (empty if unavailable)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>chapters</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>chapters</code> -{' '}
{_('Array of chapters')}
</li>
<li className='ml-4'>
<code className='bg-base-300 rounded px-1'>chapter.title</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>chapter.title</code> -{' '}
{_('Chapter title')}
</li>
<li className='ml-4'>
<code className='bg-base-300 rounded px-1'>chapter.annotations</code> -{' '}
{_('Array of annotations')}
<code className='bg-base-300 rounded-sm px-1'>chapter.annotations</code>{' '}
- {_('Array of annotations')}
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.text</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>annotation.text</code> -{' '}
{_('Highlighted text')}
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.note</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>annotation.note</code> -{' '}
{_('Annotation note')}
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.style</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>annotation.style</code> -{' '}
{_('Annotation style')}: underline | highlight | squiggly
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.color</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>annotation.color</code> -{' '}
{_('Annotation color')}: yellow | red | green | blue | violet
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.page</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>annotation.page</code> -{' '}
{_('Annotation page number')}
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.timestamp</code> -{' '}
{_('Annotation time')}
<code className='bg-base-300 rounded-sm px-1'>
annotation.timestamp
</code>{' '}
- {_('Annotation time')}
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.link</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>annotation.link</code> -{' '}
{_('Annotation link (follows the selected Link Type)')}
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.appLink</code> -{' '}
{_('App deeplink (readest://)')}
<code className='bg-base-300 rounded-sm px-1'>annotation.appLink</code>{' '}
- {_('App deeplink (readest://)')}
</li>
<li className='ml-8'>
<code className='bg-base-300 rounded px-1'>annotation.webLink</code> -{' '}
{_('Universal web link (https://)')}
<code className='bg-base-300 rounded-sm px-1'>annotation.webLink</code>{' '}
- {_('Universal web link (https://)')}
</li>
</ul>
</div>
@ -834,52 +836,56 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
<p className='mb-2 font-bold'>{_('Available Formatters:')}</p>
<ul className='space-y-1 font-mono'>
<li>
<code className='bg-base-300 rounded px-1'>date</code> /{' '}
<code className='bg-base-300 rounded px-1'>{"date('%Y-%m-%d')"}</code> -{' '}
{_('Format date')}
<code className='bg-base-300 rounded-sm px-1'>date</code> /{' '}
<code className='bg-base-300 rounded-sm px-1'>
{"date('%Y-%m-%d')"}
</code>{' '}
- {_('Format date')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>blockquote</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>blockquote</code> -{' '}
{_('Markdown block quote (> per line)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>nl2br</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>nl2br</code> -{' '}
{_('Newlines to <br>')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>upper</code> /{' '}
<code className='bg-base-300 rounded px-1'>lower</code> /{' '}
<code className='bg-base-300 rounded px-1'>capitalize</code> /{' '}
<code className='bg-base-300 rounded px-1'>title</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>upper</code> /{' '}
<code className='bg-base-300 rounded-sm px-1'>lower</code> /{' '}
<code className='bg-base-300 rounded-sm px-1'>capitalize</code> /{' '}
<code className='bg-base-300 rounded-sm px-1'>title</code> -{' '}
{_('Change case')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>trim</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>trim</code> -{' '}
{_('Trim whitespace')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>truncate(n)</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>truncate(n)</code> -{' '}
{_('Truncate to n characters')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>{"replace('a', 'b')"}</code>{' '}
<code className='bg-base-300 rounded-sm px-1'>
{"replace('a', 'b')"}
</code>{' '}
- {_('Replace text')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>default(val)</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>default(val)</code> -{' '}
{_('Fallback value')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>length</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>length</code> -{' '}
{_('Get length')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>first</code> /{' '}
<code className='bg-base-300 rounded px-1'>last</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>first</code> /{' '}
<code className='bg-base-300 rounded-sm px-1'>last</code> -{' '}
{_('First/last element')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>{"join(', ')"}</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>{"join(', ')"}</code> -{' '}
{_('Join array')}
</li>
</ul>
@ -888,27 +894,27 @@ const ExportMarkdownDialog: React.FC<ExportMarkdownDialogProps> = ({
<p className='mb-2 font-bold'>{_('Date Format Tokens:')}</p>
<ul className='space-y-1 font-mono'>
<li>
<code className='bg-base-300 rounded px-1'>%Y</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>%Y</code> -{' '}
{_('Year (4 digits)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>%m</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>%m</code> -{' '}
{_('Month (01-12)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>%d</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>%d</code> -{' '}
{_('Day (01-31)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>%H</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>%H</code> -{' '}
{_('Hour (00-23)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>%M</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>%M</code> -{' '}
{_('Minute (00-59)')}
</li>
<li>
<code className='bg-base-300 rounded px-1'>%S</code> -{' '}
<code className='bg-base-300 rounded-sm px-1'>%S</code> -{' '}
{_('Second (00-59)')}
</li>
</ul>

View file

@ -216,7 +216,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
aria-label={_('Select {{style}} style', { style: _(style) })}
onClick={() => handleSelectStyle(style)}
className={clsx(
'eink-bordered not-eink:shadow-sm flex items-center justify-center rounded-full p-0',
'eink-bordered not-eink:shadow-xs flex items-center justify-center rounded-full p-0',
'bg-base-300 theme-dark:bg-base-100',
selectedStyle === style
? 'border-current border-2'
@ -248,7 +248,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
...(style === 'squiggly' && { textDecorationStyle: 'wavy' }),
}}
className={clsx(
'decoration-inherit rounded-sm p-0 leading-none',
'decoration-inherit rounded-xs p-0 leading-none',
// The marker glyph always sets its own ink above, so it must
// stay off `text-base-content`: the e-ink rule for that class
// flattens the color with `!important`, which outranks the
@ -281,7 +281,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
title={_('Apply to every occurrence in the book')}
onClick={() => onToggleGlobal?.()}
className={clsx(
'not-eink:border-base-content/20 eink-bordered not-eink:shadow-sm flex flex-shrink-0 items-center justify-center rounded-full border p-0 transition-colors',
'not-eink:border-base-content/20 eink-bordered not-eink:shadow-xs flex shrink-0 items-center justify-center rounded-full border p-0 transition-colors',
'bg-base-300 theme-dark:bg-base-100',
globalToggleActive
? 'not-eink:text-primary'
@ -297,7 +297,7 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
ref={colorStripRef}
{...stripPointerHandlers}
className={clsx(
'not-eink:border-base-content/20 eink-bordered not-eink:shadow-sm flex items-center gap-2 rounded-3xl border',
'not-eink:border-base-content/20 eink-bordered not-eink:shadow-xs flex items-center gap-2 rounded-3xl border',
'bg-base-300 theme-dark:bg-base-100',
isVertical ? 'flex-col overflow-y-auto py-2' : 'min-w-0 flex-row overflow-x-auto px-2',
!isVertical && 'cursor-grab',

View file

@ -29,8 +29,8 @@ const ImportAnnotationsDialog: React.FC<ImportAnnotationsDialogProps> = ({
isOpen={isOpen}
title={_('Import Annotations')}
onClose={onClose}
boxClassName='sm:!h-auto sm:!max-h-[90vh] sm:!w-[420px]'
contentClassName='sm:!px-6'
boxClassName='sm:h-auto! sm:max-h-[90vh]! sm:w-[420px]!'
contentClassName='sm:px-6!'
>
<BoxedList
title={_('Import From')}

View file

@ -155,7 +155,7 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
onClick={onManage}
aria-label={_('Proofread Replacement Rules')}
title={_('Proofread Replacement Rules')}
className='shrink-0 rounded p-1 hover:bg-base-200 text-base-content/70 hover:text-base-content transition-colors'
className='shrink-0 rounded-sm p-1 hover:bg-base-200 text-base-content/70 hover:text-base-content transition-colors'
>
<RiListSettingsLine size={16} />
</button>
@ -180,7 +180,7 @@ const ProofreadPopup: React.FC<ProofreadPopupProps> = ({
}
}}
placeholder={_('Enter text...')}
className='bg-base-200 text-base-content placeholder:text-base-content/40 border-base-300 focus:border-primary focus:ring-primary eink-bordered w-full flex-1 rounded-md border p-2 text-sm transition-all focus:outline-none focus:ring-1'
className='bg-base-200 text-base-content placeholder:text-base-content/40 border-base-300 focus:border-primary focus:ring-primary eink-bordered w-full flex-1 rounded-md border p-2 text-sm transition-all focus:outline-hidden focus:ring-1'
/>
<button
onClick={handleApply}

View file

@ -176,7 +176,7 @@ const TranslatorPopup: React.FC<TranslatorPopupProps> = ({
<p className='text-base'>{text}</p>
</div>
<div className='mx-4 flex-shrink-0 border-t border-base-content/20'></div>
<div className='mx-4 shrink-0 border-t border-base-content/20'></div>
<div className='overflow-y-auto p-4 font-sans'>
<div className='mb-2 flex items-center justify-between'>

View file

@ -46,7 +46,7 @@ const AudiobookChapterPicker = ({
<div className='border-base-200 flex items-center gap-2 border-b p-2'>
<input
type='search'
className='input input-sm eink-bordered min-w-0 flex-1'
className='input input-sm eink-bordered min-w-0 flex-1 border-transparent'
aria-label={_('Search audio chapters')}
placeholder={_('Search audio chapters')}
value={query}
@ -75,7 +75,7 @@ const AudiobookChapterPicker = ({
onClick={() => onSelect('')}
>
<span className='min-w-0 flex-1'>{_('No audio')}</span>
{!value && <MdCheck className='h-5 w-5 flex-shrink-0' />}
{!value && <MdCheck className='h-5 w-5 shrink-0' />}
</button>
{filtered.map((chapter) => {
const selected = chapter.id === value;
@ -92,11 +92,11 @@ const AudiobookChapterPicker = ({
<span className='min-w-0 flex-1 truncate'>
{chapter.label} · {formatAudiobookTimecode(chapter.end - chapter.start)}
</span>
{selected && <MdCheck className='h-5 w-5 flex-shrink-0' />}
{selected && <MdCheck className='h-5 w-5 shrink-0' />}
</button>
<button
type='button'
className='btn btn-ghost btn-sm btn-square flex-shrink-0'
className='btn btn-ghost btn-sm btn-square shrink-0'
aria-label={
previewing
? _('Stop previewing {{chapter}}', { chapter: chapter.label })

View file

@ -464,7 +464,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
/>
<div className='eink-bordered border-base-200 bg-base-100 mb-5 rounded-lg border'>
<div className='flex min-h-14 items-center gap-3 px-4 py-3'>
<span className='bg-base-200 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full'>
<span className='bg-base-200 flex h-9 w-9 shrink-0 items-center justify-center rounded-full'>
{streamedFrom ? (
<MdHeadphones className='h-5 w-5' />
) : (
@ -552,7 +552,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
'eink-bordered group flex min-h-28 w-full flex-col items-center justify-center gap-3',
'border-base-200 bg-base-100 rounded-lg border px-6 py-5',
'hover:border-base-300 hover:bg-base-200/60 transition-colors duration-150',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<span className='bg-base-200 group-hover:bg-base-content group-hover:text-base-100 flex h-10 w-10 items-center justify-center rounded-full transition-colors duration-150'>
@ -570,7 +570,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
'eink-bordered group mt-4 flex min-h-28 w-full flex-col items-center justify-center gap-3',
'border-base-200 bg-base-100 rounded-lg border px-6 py-5',
'hover:border-base-300 hover:bg-base-200/60 transition-colors duration-150',
'focus-visible:ring-base-content/15 focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-base-content/15 focus-visible:outline-hidden focus-visible:ring-2',
)}
>
<span className='bg-base-200 group-hover:bg-base-content group-hover:text-base-100 flex h-10 w-10 items-center justify-center rounded-full transition-colors duration-150'>
@ -605,7 +605,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
<div className='border-base-200 border-b p-2'>
<input
type='search'
className='input input-sm eink-bordered w-full'
className='input input-sm eink-bordered w-full border-transparent'
aria-label={_('Search audiobooks')}
placeholder={_('Search audiobooks')}
value={absQuery}
@ -622,7 +622,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
disabled={busy}
onClick={() => chooseAbsBook(candidate)}
>
<span className='bg-base-200 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full'>
<span className='bg-base-200 flex h-9 w-9 shrink-0 items-center justify-center rounded-full'>
<MdHeadphones className='h-5 w-5' />
</span>
<span className='min-w-0 flex-1'>
@ -674,7 +674,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
<div className='hover:bg-base-200/60 focus-within:bg-base-200/60 flex w-full items-center rounded-md sm:max-w-[60%]'>
<select
id='audiobook-ebook-anchor'
className='select h-9 min-w-0 flex-1 cursor-pointer !appearance-none truncate !border-0 !bg-transparent !bg-none !pe-1 !ps-2 text-end focus:!border-0 focus:!shadow-none focus:!outline-none focus:!ring-0'
className='select h-9 min-w-0 flex-1 cursor-pointer truncate border-0! bg-transparent! bg-none! pe-1! ps-2! text-end focus:border-0! focus:shadow-none! focus:outline-hidden! focus:ring-0! open:outline-hidden!'
value={selectedEbookChapterId}
onChange={(event) => setSelectedEbookChapterId(event.target.value)}
>
@ -686,7 +686,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
</select>
<MdArrowDropDown
aria-hidden='true'
className='text-base-content/55 pointer-events-none h-5 w-5 flex-shrink-0'
className='text-base-content/55 pointer-events-none h-5 w-5 shrink-0'
/>
</div>
</div>
@ -698,7 +698,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
<div className='hover:bg-base-200/60 focus-within:bg-base-200/60 flex min-w-0 flex-1 items-center rounded-md'>
<select
id='audiobook-audio-anchor'
className='select h-9 min-w-0 flex-1 cursor-pointer !appearance-none truncate !border-0 !bg-transparent !bg-none !pe-1 !ps-2 text-end focus:!border-0 focus:!shadow-none focus:!outline-none focus:!ring-0'
className='select h-9 min-w-0 flex-1 cursor-pointer truncate border-0! bg-transparent! bg-none! pe-1! ps-2! text-end focus:border-0! focus:shadow-none! focus:outline-hidden! focus:ring-0! open:outline-hidden!'
value={selectedAudioChapterId}
onChange={(event) => setSelectedAudioChapterId(event.target.value)}
>
@ -710,13 +710,13 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
</select>
<MdArrowDropDown
aria-hidden='true'
className='text-base-content/55 pointer-events-none h-5 w-5 flex-shrink-0'
className='text-base-content/55 pointer-events-none h-5 w-5 shrink-0'
/>
</div>
{audioChapterById.get(selectedAudioChapterId) && (
<button
type='button'
className='btn btn-ghost btn-sm btn-square flex-shrink-0'
className='btn btn-ghost btn-sm btn-square shrink-0'
aria-label={
previewingAudioChapterId === selectedAudioChapterId
? _('Stop audio preview')
@ -820,12 +820,12 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
? `${mappedAudio.label} · ${formatAudiobookTimecode(mappedAudio.end - mappedAudio.start)}`
: _('No audio')}
</span>
<MdArrowDropDown className='h-5 w-5 flex-shrink-0' />
<MdArrowDropDown className='h-5 w-5 shrink-0' />
</button>
{mappedAudio && (
<button
type='button'
className='btn btn-ghost btn-sm btn-square flex-shrink-0'
className='btn btn-ghost btn-sm btn-square shrink-0'
aria-label={
previewingAudioChapterId === mappedAudio.id
? _('Stop previewing {{chapter}}', { chapter: mappedAudio.label })
@ -886,7 +886,7 @@ const AudiobookPairingDialog = ({ bookKey, bookDoc, onClose }: AudiobookPairingD
title={association ? _('Manage Audiobook') : _('Pair Audiobook')}
onClose={onClose}
boxClassName='sm:h-[80%] sm:min-w-[620px] sm:max-w-[720px]'
contentClassName='!px-6 sm:!px-8'
contentClassName='px-6! sm:px-8!'
useOverlayScroll
>
<div className='pb-6 pt-2'>

View file

@ -142,7 +142,7 @@ export const ColorPanel: React.FC<ColorPanelProps> = ({
key={name}
onClick={() => setThemeColor(name)}
className={clsx(
'flex flex-shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'flex shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'h-[40px] min-w-[80px]',
themeColor === name
? 'ring-primary ring-offset-base-200 ring-2 ring-offset-2'
@ -159,7 +159,7 @@ export const ColorPanel: React.FC<ColorPanelProps> = ({
<button
onClick={() => cycleThemeMode()}
className={clsx(
'flex flex-shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'flex shrink-0 flex-col items-center justify-center rounded-lg p-3 transition-all',
'h-[40px] min-w-[80px]',
themeMode === 'dark'
? 'ring-primary ring-offset-base-200 ring-2 ring-offset-2'

View file

@ -225,7 +225,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
!forceMobileLayout &&
(needHorizontalScroll ? 'sm:!bottom-3 sm:!h-10 sm:justify-end' : 'sm:justify-center'),
(needHorizontalScroll ? 'sm:bottom-3! sm:h-10! sm:justify-end' : 'sm:justify-center'),
isVisible
? 'pointer-events-auto translate-y-0 opacity-100'
: forceMobileLayout
@ -243,7 +243,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
tabIndex={-1}
className={clsx(
'absolute bottom-0 left-0 z-10 flex h-[52px] w-full',
needHorizontalScroll && 'sm:!bottom-3 sm:!h-7',
needHorizontalScroll && 'sm:bottom-3! sm:h-7!',
isMobile || pointerInDoc ? 'pointer-events-none' : '',
)}
onMouseEnter={() => !isMobile && setHoveredBookKey(bookKey)}

View file

@ -127,7 +127,7 @@ const PageJumpInput: React.FC<PageJumpInputProps> = ({ bookKey, showFraction, cl
title={_('Go to Page')}
aria-label={_('Go to Page')}
value={text}
className='absolute inset-0 h-full w-full bg-transparent text-center focus:outline-none'
className='absolute inset-0 h-full w-full bg-transparent text-center focus:outline-hidden'
onFocus={() => {
// Keep the "current / total" text as-is (percentage style swaps to
// it) and pre-select the page portion for type-over editing.

View file

@ -176,7 +176,7 @@ const HardcoverLinkDialog = ({ bookKey, onClose }: HardcoverLinkDialogProps) =>
title={_('Link Hardcover Book')}
onClose={onClose}
boxClassName='sm:h-[80%] sm:min-w-[560px] sm:max-w-[680px]'
contentClassName='!px-6 sm:!px-8'
contentClassName='px-6! sm:px-8!'
useOverlayScroll
>
<div className='pb-6 pt-2'>
@ -200,7 +200,7 @@ const HardcoverLinkDialog = ({ bookKey, onClose }: HardcoverLinkDialogProps) =>
</div>
<button
type='button'
className='btn btn-ghost btn-sm text-error flex-shrink-0'
className='btn btn-ghost btn-sm text-error shrink-0'
disabled={busy}
onClick={unlink}
>
@ -218,7 +218,7 @@ const HardcoverLinkDialog = ({ bookKey, onClose }: HardcoverLinkDialogProps) =>
>
<input
type='search'
className='input input-bordered eink-bordered settings-content h-10 min-w-0 flex-1 focus:outline-none'
className='input eink-bordered settings-content h-10 min-w-0 flex-1 focus:outline-hidden'
placeholder={_('Search Hardcover')}
aria-label={_('Search Hardcover')}
spellCheck='false'
@ -227,7 +227,7 @@ const HardcoverLinkDialog = ({ bookKey, onClose }: HardcoverLinkDialogProps) =>
/>
<button
type='submit'
className='btn btn-contrast h-10 min-h-10 flex-shrink-0'
className='btn btn-contrast h-10 min-h-10 shrink-0'
disabled={searching || !query.trim()}
>
{searching ? (
@ -267,14 +267,14 @@ const HardcoverLinkDialog = ({ bookKey, onClose }: HardcoverLinkDialogProps) =>
type='button'
className={clsx(
'hover:bg-base-200/60 flex w-full items-center gap-3 px-4 py-3 text-start',
'focus-visible:bg-base-200/60 focus-visible:outline-none',
'focus-visible:bg-base-200/60 focus-visible:outline-hidden',
'disabled:cursor-not-allowed disabled:opacity-60',
)}
aria-pressed={isLinked}
disabled={busy}
onClick={() => linkTo(candidate)}
>
<span className='bg-base-200 text-base-content/55 flex h-14 w-10 flex-shrink-0 items-center justify-center overflow-hidden rounded-sm'>
<span className='bg-base-200 text-base-content/55 flex h-14 w-10 shrink-0 items-center justify-center overflow-hidden rounded-xs'>
{candidate.coverUrl ? (
<img
src={candidate.coverUrl}
@ -302,9 +302,7 @@ const HardcoverLinkDialog = ({ bookKey, onClose }: HardcoverLinkDialogProps) =>
</span>
)}
</span>
{isLinked && (
<MdCheck className='h-5 w-5 flex-shrink-0' aria-label={_('Linked')} />
)}
{isLinked && <MdCheck className='h-5 w-5 shrink-0' aria-label={_('Linked')} />}
</button>
</li>
);

View file

@ -391,7 +391,7 @@ const Notebook: React.FC = ({}) => {
onTouchStart={handleDragStart}
onKeyDown={handleDragKeyDown}
/>
<div className='flex-shrink-0'>
<div className='shrink-0'>
{isMobile && (
<div
role='slider'
@ -434,7 +434,7 @@ const Notebook: React.FC = ({}) => {
<AIAssistant key={activeConversationId ?? 'new'} bookKey={sideBarBookKey} />
</div>
) : isNotesTabEmpty ? (
<div className='flex flex-grow items-center justify-center overflow-y-auto px-3'>
<div className='flex grow items-center justify-center overflow-y-auto px-3'>
<EmptyState
Icon={RiQuillPenLine}
label={_('No Notes')}
@ -442,7 +442,7 @@ const Notebook: React.FC = ({}) => {
/>
</div>
) : (
<div className='flex-grow overflow-y-auto px-3'>
<div className='grow overflow-y-auto px-3'>
{isSearchBarVisible && searchResults && !hasSearchResults && hasAnyNotes && (
<div className='flex h-32 items-center justify-center text-gray-500'>
<p className='font-size-sm text-center'>{_('No notes match your search')}</p>
@ -515,7 +515,7 @@ const Notebook: React.FC = ({}) => {
</div>
)}
<div
className='flex-shrink-0'
className='shrink-0'
style={{
paddingBottom: `${(safeAreaInsets?.bottom || 0) / 2}px`,
}}

View file

@ -120,7 +120,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
spellCheck={false}
onChange={handleInputChange}
placeholder={_('Search excerpts...')}
className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-none'
className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-hidden'
/>
{searchTerm && (

View file

@ -552,7 +552,7 @@ const ParagraphOverlay: React.FC<ParagraphOverlayProps> = ({
// The dialog is focused programmatically (so it receives keys); it is not
// a tab stop, so suppress the focus ring that would otherwise outline the
// whole viewport.
'outline-none',
'outline-hidden',
'transition-opacity duration-300 ease-out',
isOverlayMounted ? 'opacity-100' : 'opacity-0',
)}

View file

@ -140,7 +140,7 @@ const WpmEntry: React.FC<WpmEntryProps> = ({ controller, wpm, bgColor }) => {
enterKeyHint='done'
aria-label={_('Words per minute')}
value={draft ?? String(wpm)}
className='eink-bordered w-14 rounded-md border border-gray-500/20 bg-gray-500/10 px-1 py-0.5 text-center text-sm font-semibold tabular-nums focus:outline-none'
className='eink-bordered w-14 rounded-md border border-gray-500/20 bg-gray-500/10 px-1 py-0.5 text-center text-sm font-semibold tabular-nums focus:outline-hidden'
onFocus={(e) => e.target.select()}
onChange={(e) => {
if (/^\d*$/.test(e.target.value)) setDraft(e.target.value);
@ -1130,7 +1130,7 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
aria-valuenow={Math.round(state.progress)}
aria-valuemin={0}
aria-valuemax={100}
className='relative h-2 cursor-pointer overflow-visible rounded bg-gray-500/30'
className='relative h-2 cursor-pointer overflow-visible rounded-sm bg-gray-500/30'
// touch-action: none keeps mobile browsers from claiming the
// gesture for scroll/pan, which would fire pointercancel and
// break the drag-to-seek pointer capture mid-gesture.
@ -1148,11 +1148,11 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
title={_('Drag to seek')}
>
<div
className={`absolute left-0 top-0 h-full rounded ${isProgressBarDragging ? '' : 'transition-[width] duration-100'}`}
className={`absolute left-0 top-0 h-full rounded-sm ${isProgressBarDragging ? '' : 'transition-[width] duration-100'}`}
style={{ width: `${state.progress}%`, backgroundColor: accentColor }}
/>
<div
className={`absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full shadow ${isProgressBarDragging ? '' : 'transition-[left] duration-100'}`}
className={`absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full shadow-sm ${isProgressBarDragging ? '' : 'transition-[left] duration-100'}`}
style={{ left: `${state.progress}%`, backgroundColor: accentColor }}
/>
</div>
@ -1287,7 +1287,7 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
<label className='flex cursor-pointer items-center gap-1.5 font-medium opacity-80'>
<span className='mr-0.5 font-medium opacity-50'>{_('Punctuation Delay')}</span>
<select
className='cursor-pointer rounded border border-gray-500/30 bg-gray-500/20 px-1.5 py-1 text-xs font-medium transition-colors hover:border-gray-500/40 hover:bg-gray-500/30'
className='cursor-pointer rounded-sm border border-gray-500/30 bg-gray-500/20 px-1.5 py-1 text-xs font-medium transition-colors hover:border-gray-500/40 hover:bg-gray-500/30'
style={{ color: 'inherit' }}
value={state.punctuationPauseMs}
onChange={(e) => controller.setPunctuationPause(parseInt(e.target.value, 10))}
@ -1305,7 +1305,7 @@ const RSVPOverlay: React.FC<RSVPOverlayProps> = ({
<span className='mr-0.5 font-medium opacity-50'>{_('Start Delay')}</span>
<select
data-testid='rsvp-start-delay-select'
className='cursor-pointer rounded border border-gray-500/30 bg-gray-500/20 px-1.5 py-1 text-xs font-medium transition-colors hover:border-gray-500/40 hover:bg-gray-500/30'
className='cursor-pointer rounded-sm border border-gray-500/30 bg-gray-500/20 px-1.5 py-1 text-xs font-medium transition-colors hover:border-gray-500/40 hover:bg-gray-500/30'
style={{ color: 'inherit' }}
value={state.startDelaySeconds}
onChange={(e) => controller.setStartDelay(parseInt(e.target.value, 10))}

View file

@ -266,7 +266,7 @@ const AnnotationsToolbar: React.FC<AnnotationsToolbarProps> = ({
if (e.key === 'Escape') onCloseSearch();
}}
placeholder={_('Search annotations...')}
className='w-full min-w-0 bg-transparent p-2 font-sans text-sm font-light focus:outline-none'
className='w-full min-w-0 bg-transparent p-2 font-sans text-sm font-light focus:outline-hidden'
/>
<button
onClick={onCloseSearch}

View file

@ -38,7 +38,7 @@ const BookCard = ({ book }: { book: Book }) => {
type='button'
aria-label={_('View Book Cover')}
className={clsx(
'me-4 aspect-[28/41] max-h-16 w-[15%] max-w-12 overflow-hidden rounded-sm shadow-md',
'me-4 aspect-[28/41] max-h-16 w-[15%] max-w-12 overflow-hidden rounded-xs shadow-md',
isDarkMode ? 'mix-blend-screen' : 'mix-blend-multiply',
)}
onClick={openCoverViewer}
@ -48,7 +48,7 @@ const BookCard = ({ book }: { book: Book }) => {
mode='list'
coverFit='crop'
showSpine={settings.librarySkeuomorphicCovers}
imageClassName='rounded-sm'
imageClassName='rounded-xs'
onImageError={() => (bookCoverRef.current!.style.display = 'none')}
/>
</button>

View file

@ -166,7 +166,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
alt={book.title}
width={56}
height={80}
className='aspect-auto max-h-8 max-w-4 rounded-sm shadow-md'
className='aspect-auto max-h-8 max-w-4 rounded-xs shadow-md'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}

View file

@ -160,7 +160,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({
>
<div className='flex w-full'>
<TextEditor
className='!leading-normal'
className='leading-normal!'
ref={editorRef}
value={draftText}
onChange={setDraftText}

View file

@ -371,7 +371,7 @@ const BooknoteView: React.FC<{
const isEmpty = sortedGroups.length === 0;
return (
<div className='booknote-list rounded' role='tree'>
<div className='booknote-list rounded-sm' role='tree'>
{type === 'annotation' && (
<AnnotationsToolbar
filterKind={filterKind}

View file

@ -166,10 +166,7 @@ const ChatHistoryView: React.FC<ChatHistoryViewProps> = ({ bookKey }) => {
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
onKeyDown={handleKeyDown}
className={clsx(
'input input-xs input-bordered w-full',
'bg-base-100 text-base-content',
)}
className={clsx('input input-xs w-full', 'bg-base-100 text-base-content')}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
@ -202,7 +199,7 @@ const ChatHistoryView: React.FC<ChatHistoryViewProps> = ({ bookKey }) => {
</div>
{editingId !== conversation.id && (
<div className='flex flex-shrink-0 gap-0.5 opacity-0 transition-opacity group-hover:opacity-100'>
<div className='flex shrink-0 gap-0.5 opacity-0 transition-opacity group-hover:opacity-100'>
<button
onClick={(e) => handleStartRename(e, conversation)}
className='btn btn-ghost btn-xs'

View file

@ -73,7 +73,7 @@ const SidebarContent: React.FC<{
<>
<div
className={clsx(
'sidebar-content flex h-full min-h-0 flex-grow flex-col shadow-inner',
'sidebar-content flex h-full min-h-0 grow flex-col shadow-inner',
'font-sans text-base font-normal sm:text-sm',
)}
>
@ -115,7 +115,7 @@ const SidebarContent: React.FC<{
)}
</div>
<div
className='flex-shrink-0'
className='shrink-0'
style={
{
// paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) / 2)',

View file

@ -64,7 +64,7 @@ const SidebarHeader: React.FC<{
window.innerWidth < 640 ? 'dropdown-end' : 'dropdown-center',
'dropdown-bottom',
)}
menuClassName={clsx('no-triangle mt-1', window.innerWidth < 640 ? '' : '!relative')}
menuClassName={clsx('no-triangle mt-1', window.innerWidth < 640 ? '' : 'relative!')}
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
containerClassName='h-8'
toggleButton={<MdOutlineMenu className='fill-base-content' />}

View file

@ -356,7 +356,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
? _('Words to find near each other')
: _('Search...')
}
className='search-input w-full bg-transparent p-2 pr-0 ps-10 font-sans text-sm font-light focus:outline-none'
className='search-input w-full bg-transparent p-2 pr-0 ps-10 font-sans text-sm font-light focus:outline-hidden'
/>
{searchTerm && (
@ -381,10 +381,10 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
window.innerWidth < 640 ? 'dropdown-end' : 'dropdown-center',
'dropdown-bottom',
)}
menuClassName={clsx('no-triangle mt-1', window.innerWidth < 640 ? '' : '!relative')}
menuClassName={clsx('no-triangle mt-1', window.innerWidth < 640 ? '' : 'relative!')}
buttonClassName={clsx(
'btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg',
viewSettings?.isEink ? '!bg-transparent hover:!bg-transparent' : '',
viewSettings?.isEink ? 'bg-transparent! hover:bg-transparent!' : '',
)}
toggleButton={<FaChevronDown size={iconSize12} className='text-base-content/50' />}
>
@ -403,7 +403,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
<div className='relative flex'>
<div
className={clsx(
'from-base-200 pointer-events-none absolute left-0 top-0 h-full w-3 bg-gradient-to-r to-transparent',
'from-base-200 pointer-events-none absolute left-0 top-0 h-full w-3 bg-linear-to-r to-transparent',
viewSettings?.isEink ? 'hidden' : '',
)}
aria-hidden='true'
@ -416,7 +416,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
<button
key={index}
onClick={() => handleHistoryClick(term)}
className='hover:bg-base-200/20 text-base-content/70 bg-base-100 max-w-[60%] flex-shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
className='hover:bg-base-200/20 text-base-content/70 bg-base-100 max-w-[60%] shrink-0 whitespace-nowrap rounded-full px-3 py-0.5 text-xs'
>
<p className='truncate'>{term}</p>
</button>
@ -424,7 +424,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
</div>
<div
className={clsx(
'from-base-200 pointer-events-none absolute right-6 top-0 h-full w-6 bg-gradient-to-l to-transparent',
'from-base-200 pointer-events-none absolute right-6 top-0 h-full w-6 bg-linear-to-l to-transparent',
viewSettings?.isEink ? 'hidden' : '',
)}
aria-hidden='true'
@ -432,7 +432,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ isVisible, bookKey, onHideSearchB
<button
onClick={handleClearHistory}
className={clsx(
'text-base-content/50 hover:text-base-content/80 flex-shrink-0 items-center',
'text-base-content/50 hover:text-base-content/80 shrink-0 items-center',
'flex h-6 min-h-6 w-8 min-w-8 items-center justify-center p-0',
)}
title={_('Clear search history')}

View file

@ -78,7 +78,7 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
// line. `.dropdown-content` sizes the box to its content and caps it at
// the viewport; the max-height keeps a tall menu scrollable in landscape.
'max-h-[calc(100vh-96px)] overflow-y-auto',
isEink ? 'bordercolor-content border-base-content !bg-base-100 border' : '',
isEink ? 'bordercolor-content border-base-content bg-base-100! border' : '',
menuClassName,
)}
>

View file

@ -221,7 +221,7 @@ const ChapterSection: React.FC<ChapterSectionProps> = ({
>
<button
type='button'
className='not-eink:hover:bg-base-300 focus-visible:ring-base-content/15 flex w-full select-none items-center justify-between rounded px-1 py-1 text-start focus-visible:outline-none focus-visible:ring-2'
className='not-eink:hover:bg-base-300 focus-visible:ring-base-content/15 flex w-full select-none items-center justify-between rounded-sm px-1 py-1 text-start focus-visible:outline-hidden focus-visible:ring-2'
onClick={handleToggle}
aria-expanded={isExpanded}
>

View file

@ -236,7 +236,7 @@ const SideBar = ({}) => {
onTouchStart={handleHorizontalDragStart}
onKeyDown={handleDragKeyDown}
></div>
<div className='flex-shrink-0'>
<div className='shrink-0'>
{isMobile && (
<div
role='slider'

Some files were not shown because too many files have changed in this diff Show more