mirror of
https://github.com/readest/readest.git
synced 2026-10-19 10:13:29 +00:00
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:
parent
2fd8b3bc92
commit
8ef527af70
185 changed files with 2746 additions and 2201 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"config": "",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
33
apps/readest-app/src/__tests__/styles/daisyui-themes.test.ts
Normal file
33
apps/readest-app/src/__tests__/styles/daisyui-themes.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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' />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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' />
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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]}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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' ||
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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'>)
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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))}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -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' />}
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue