chore: migrate to tailwind v4
continuous-integration/drone/pr Build is failing

This commit is contained in:
2026-07-03 14:53:39 -04:00
parent c9e4d345b8
commit 2c2eca8360
33 changed files with 4590 additions and 1615 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
root = "."
tmp_dir = "_scratch/air"
tmp_dir = "tmp"
[build]
cmd = "go build -o ./_scratch/air/antholume ."
full_bin = "./_scratch/air/antholume serve"
cmd = "go build -o ./tmp/antholume ."
full_bin = "./tmp/antholume serve"
delay = 1000
include_ext = ["go", "tpl", "tmpl", "html", "css", "js", "yaml", "yml"]
exclude_dir = ["_scratch", "build", "data", "frontend", "node_modules"]
+3 -3
View File
@@ -22,7 +22,7 @@ Edit:
Regenerate:
- `go generate ./api/v1/generate.go`
- `cd frontend && bun run generate:api`
- `cd frontend && pnpm run generate:api`
Notes:
- If you add response headers in `api/v1/openapi.yaml` (for example `Set-Cookie`), `oapi-codegen` will generate typed response header structs in `api/v1/api.gen.go`; update the handler response values to populate those headers explicitly.
@@ -51,7 +51,7 @@ Regenerate:
### Notes
- The Go server embeds `templates/*` and `assets/*`.
- Root Tailwind output is built to `assets/style.css`.
- Root Tailwind output is built to `assets/style.css`. Two separate Tailwind setups exist: the legacy server-rendered app uses **Tailwind v3** via the root `tailwind.config.js` + `make legacy_tailwind`; the React app in `frontend/` uses **Tailwind v4** (CSS-first, no config file). Keep them distinct.
- Be mindful of whether a change affects the embedded server-rendered app, the React frontend, or both.
- SQLite timestamps are stored as RFC3339 strings (usually with a trailing `Z`); prefer `parseTime` / `parseTimePtr` instead of ad-hoc `time.Parse` layouts.
- `DISABLE_AUTH=true` bypasses authentication on **all** routes (v1 API, legacy web app, KOSync, OPDS). Set `DISABLE_AUTH_USER=<username>` to control which database user the session impersonates (defaults to the first user in the DB). The user must already exist.
@@ -64,7 +64,7 @@ For frontend-specific implementation notes and commands, also read:
## 6) Regeneration Summary
- Go API: `go generate ./api/v1/generate.go`
- Frontend API client: `cd frontend && bun run generate:api`
- Frontend API client: `cd frontend && pnpm run generate:api`
- SQLC: `sqlc generate`
## 7) Live Dev Server Debugging
+1 -1
View File
@@ -49,7 +49,7 @@ dev_backend:
$(DEV_ENV) air
dev_frontend:
cd frontend && bun run dev
cd frontend && pnpm run dev
clean:
rm -rf ./build
+1 -1
View File
@@ -25,8 +25,8 @@
golangci-lint
gopls
bun
nodejs
pnpm
tailwindcss
typescript-language-server
];
+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"jsPlugins": ["oxlint-tailwindcss"],
"ignorePatterns": ["src/generated/**", "dist/**"],
"plugins": ["react", "typescript", "oxc"],
"settings": {
"tailwindcss": {
"entryPoint": "src/index.css"
}
},
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_",
"ignoreRestSiblings": true
}
],
"typescript/no-explicit-any": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"tailwindcss/no-duplicate-classes": "error",
"tailwindcss/no-conflicting-classes": "error",
"tailwindcss/no-deprecated-classes": "error",
"tailwindcss/no-unnecessary-whitespace": "warn",
"tailwindcss/no-unknown-classes": "off"
}
}
+20 -16
View File
@@ -5,11 +5,12 @@ Also follow the repository root guide at `../AGENTS.md`.
## 1) Stack
- Package manager: `bun`
- Package manager: `pnpm`
- Framework: React + Vite
- Data fetching: React Query
- API generation: Orval
- Linting: ESLint + Tailwind plugin
- Styling: Tailwind CSS v4 (CSS-first config)
- Linting: oxlint (config in `.oxlintrc.json`) + `oxlint-tailwindcss` for Tailwind rules
- Formatting: Prettier
## 2) Conventions
@@ -20,14 +21,15 @@ Also follow the repository root guide at `../AGENTS.md`.
- Avoid custom class names in JSX `className` values unless the Tailwind lint config already allows them.
- For decorative icons in inputs or labels, disable hover styling via the icon component API rather than overriding it ad hoc.
- Prefer `LoadingState` for result-area loading indicators; avoid early returns that unmount search/filter forms during fetches.
- Use theme tokens from `tailwind.config.js` / `src/index.css` (`bg-surface`, `text-content`, `border-border`, `primary`, etc.) for new UI work instead of adding raw light/dark color pairs.
- Use theme tokens defined in `src/index.css` `@theme` (`bg-surface`, `text-content`, `border-border`, `primary`, etc.) for new UI work instead of adding raw light/dark color pairs. There is no `tailwind.config.js` — Tailwind v4 config is CSS-first.
- Semantic colors map to runtime CSS variables (`--color-x: rgb(var(--x))`) via `@theme inline`; light/dark values live in `:root` / `.dark` in `src/index.css`. Dark mode is class-based via `@custom-variant dark`, toggled by `ThemeProvider`.
- Store frontend-only preferences in `src/utils/localSettings.ts` so appearance and view settings share one local-storage shape.
## 3) Generated API client
- Do not edit `src/generated/**` directly.
- Edit `../api/v1/openapi.yaml` and regenerate instead.
- Regenerate with: `bun run generate:api`
- Regenerate with: `pnpm run generate:api`
### Important behavior
@@ -43,25 +45,27 @@ Also follow the repository root guide at `../AGENTS.md`.
## 5) Commands
- Lint: `bun run lint`
- Typecheck: `bun run typecheck`
- Lint fix: `bun run lint:fix`
- Format check: `bun run format`
- Format fix: `bun run format:fix`
- Build: `bun run build`
- Generate API client: `bun run generate:api`
- Lint: `pnpm run lint`
- Typecheck: `pnpm run typecheck`
- Lint fix: `pnpm run lint:fix`
- Format check: `pnpm run format`
- Format fix: `pnpm run format:fix`
- Build: `pnpm run build`
- Generate API client: `pnpm run generate:api`
## 6) Validation Notes
- ESLint ignores `src/generated/**`.
- oxlint ignores `src/generated/**` and `dist/**` (via `ignorePatterns` in `.oxlintrc.json`).
- `lint` runs `oxlint --max-warnings=0`; keep the tree warning-free. `react-hooks/exhaustive-deps` is enforced — fix deps rather than disabling the rule; use a justified inline `// oxlint-disable-next-line` only for genuine init-once effects.
- The Tailwind lint plugin uses oxlint `jsPlugins` (experimental, not run by the editor language server), so Tailwind diagnostics surface via CLI/CI, not in-editor. It reads the theme from `src/index.css` (`settings.tailwindcss.entryPoint`).
- Frontend unit tests use Vitest and live alongside source as `src/**/*.test.ts(x)`.
- Read `TESTING_STRATEGY.md` before adding or expanding frontend tests.
- Prefer tests for meaningful app behavior, branching logic, side effects, and user-visible outcomes.
- Avoid low-value tests that mainly assert exact styling classes, duplicate existing coverage, or re-test framework/library behavior.
- `bun run lint` includes test files but does not typecheck.
- Use `bun run typecheck` to run TypeScript validation for app code and colocated tests without a full production build.
- Run frontend tests with `bun run test`.
- `bun run build` still runs `tsc && vite build`, so unrelated TypeScript issues elsewhere in `src/` can fail the build.
- `pnpm run lint` includes test files but does not typecheck.
- Use `pnpm run typecheck` to run TypeScript validation for app code and colocated tests without a full production build.
- Run frontend tests with `pnpm run test`.
- `pnpm run build` still runs `tsc && vite build`, so unrelated TypeScript issues elsewhere in `src/` can fail the build.
- When possible, validate changed files directly before escalating to full-project fixes.
## 7) Live Dev Server Debugging
+5 -5
View File
@@ -68,7 +68,7 @@ The frontend mirrors the existing SSR templates structure:
The frontend uses **Orval** to generate TypeScript types and React Query hooks from the OpenAPI spec:
```bash
npm run generate:api
pnpm run generate:api
```
This generates:
@@ -80,16 +80,16 @@ This generates:
```bash
# Install dependencies
npm install
pnpm install
# Generate API types (if OpenAPI spec changes)
npm run generate:api
pnpm run generate:api
# Start development server
npm run dev
pnpm run dev
# Build for production
npm run build
pnpm run build
```
## Deployment
+3 -3
View File
@@ -68,6 +68,6 @@ Trim or remove tests that are brittle, duplicated, or mostly validate tooling ra
For frontend test work, validate with:
- `cd frontend && bun run lint`
- `cd frontend && bun run typecheck`
- `cd frontend && bun run test`
- `cd frontend && pnpm run lint`
- `cd frontend && pnpm run typecheck`
- `cd frontend && pnpm run test`
-1350
View File
File diff suppressed because it is too large Load Diff
-82
View File
@@ -1,82 +0,0 @@
import js from "@eslint/js";
import typescriptParser from "@typescript-eslint/parser";
import typescriptPlugin from "@typescript-eslint/eslint-plugin";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import tailwindcss from "eslint-plugin-tailwindcss";
import prettier from "eslint-plugin-prettier";
import eslintConfigPrettier from "eslint-config-prettier";
export default [
js.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
ignores: ["**/generated/**"],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
projectService: true,
},
globals: {
localStorage: "readonly",
sessionStorage: "readonly",
document: "readonly",
window: "readonly",
setTimeout: "readonly",
clearTimeout: "readonly",
setInterval: "readonly",
clearInterval: "readonly",
HTMLElement: "readonly",
HTMLDivElement: "readonly",
HTMLButtonElement: "readonly",
HTMLAnchorElement: "readonly",
MouseEvent: "readonly",
Node: "readonly",
File: "readonly",
Blob: "readonly",
FormData: "readonly",
alert: "readonly",
confirm: "readonly",
prompt: "readonly",
React: "readonly",
},
},
plugins: {
"@typescript-eslint": typescriptPlugin,
react: reactPlugin,
"react-hooks": reactHooksPlugin,
tailwindcss,
prettier,
},
rules: {
...eslintConfigPrettier.rules,
...tailwindcss.configs.recommended.rules,
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"no-console": ["warn", { allow: ["warn", "error"] }],
"no-undef": "off",
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"no-useless-catch": "off",
},
settings: {
react: {
version: "detect",
},
},
},
];
+11 -14
View File
@@ -9,8 +9,8 @@
"build": "tsc && vite build",
"preview": "vite preview",
"generate:api": "orval",
"lint": "eslint src --max-warnings=0",
"lint:fix": "eslint src --fix",
"lint": "oxlint --max-warnings=0",
"lint:fix": "oxlint --fix",
"format": "prettier --check src",
"format:fix": "prettier --write src",
"test": "vitest run"
@@ -29,28 +29,25 @@
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
"@tailwindcss/vite": "^4.3.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.8",
"@typescript-eslint/eslint-plugin": "^8.13.0",
"@typescript-eslint/parser": "^8.13.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-tailwindcss": "^3.18.2",
"jsdom": "^29.0.1",
"postcss": "^8.4.49",
"oxlint": "^1.72.0",
"oxlint-tailwindcss": "^1.3.4",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.17",
"tailwindcss": "^4.3.2",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vitest": "^4.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}
+4233
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
allowBuilds:
core-js: set this to true or false
es5-ext: set this to true or false
esbuild: set this to true or false
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
+2 -2
View File
@@ -27,7 +27,7 @@ export function Pagination({
<button
type="button"
onClick={() => onPageChange(previousPage)}
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-hidden"
>
</button>
@@ -41,7 +41,7 @@ export function Pagination({
<button
type="button"
onClick={() => onPageChange(nextPage)}
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-none"
className="w-24 rounded bg-surface p-2 text-center text-sm font-medium shadow-lg hover:bg-surface-strong focus:outline-hidden"
>
</button>
+1 -1
View File
@@ -80,7 +80,7 @@ export function Table<T extends object>({
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
<table className="min-w-full bg-surface">
<thead>
<tr className="border-b border-border">
+4 -4
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { InfoIcon, WarningIcon, ErrorIcon, CloseIcon } from '../icons';
export type ToastType = 'info' | 'warning' | 'error';
@@ -42,20 +42,20 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp
const { baseStyles, typeStyles, iconStyles, textStyles } = getToastStyles(type);
const handleClose = () => {
const handleClose = useCallback(() => {
setIsAnimatingOut(true);
setTimeout(() => {
setIsVisible(false);
onClose?.(id);
}, 300);
};
}, [id, onClose]);
useEffect(() => {
if (duration > 0) {
const timer = setTimeout(handleClose, duration);
return () => clearTimeout(timer);
}
}, [duration]);
}, [duration, handleClose]);
if (!isVisible) {
return null;
+2
View File
@@ -181,6 +181,8 @@ export function useEpubReader({
URL.revokeObjectURL(objectUrl);
}
};
// Init Reader Once - Theme values seed the constructor; the effect below (applyThemeChange) handles later changes, so re-running here would needlessly destroy and recreate the reader.
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, [deviceId, deviceName, documentId, initialProgress, viewerNode]);
useEffect(() => {
+134 -3
View File
@@ -1,6 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
/* Class-Based Dark Mode - v4 defaults to prefers-color-scheme; ThemeProvider toggles `.dark`. */
@custom-variant dark (&:where(.dark, .dark *));
:root {
--white: 255 255 255;
@@ -184,7 +185,137 @@
--error-foreground: 255 255 255;
}
@theme inline {
--color-canvas: rgb(var(--canvas));
--color-surface: rgb(var(--surface));
--color-surface-muted: rgb(var(--surface-muted));
--color-surface-strong: rgb(var(--surface-strong));
--color-overlay: rgb(var(--overlay));
--color-content: rgb(var(--content));
--color-content-muted: rgb(var(--content-muted));
--color-content-subtle: rgb(var(--content-subtle));
--color-content-inverse: rgb(var(--content-inverse));
--color-border: rgb(var(--border));
--color-border-muted: rgb(var(--border-muted));
--color-border-strong: rgb(var(--border-strong));
--color-white: rgb(var(--white));
--color-black: rgb(var(--black));
--color-gray-50: rgb(var(--neutral-50));
--color-gray-100: rgb(var(--neutral-100));
--color-gray-200: rgb(var(--neutral-200));
--color-gray-300: rgb(var(--neutral-300));
--color-gray-400: rgb(var(--neutral-400));
--color-gray-500: rgb(var(--neutral-500));
--color-gray-600: rgb(var(--neutral-600));
--color-gray-700: rgb(var(--neutral-700));
--color-gray-800: rgb(var(--neutral-800));
--color-gray-900: rgb(var(--neutral-900));
--color-gray: rgb(var(--neutral-500));
--color-gray-foreground: rgb(var(--neutral-foreground));
--color-purple-50: rgb(var(--primary-50));
--color-purple-100: rgb(var(--primary-100));
--color-purple-200: rgb(var(--primary-200));
--color-purple-300: rgb(var(--primary-300));
--color-purple-400: rgb(var(--primary-400));
--color-purple-500: rgb(var(--primary-500));
--color-purple-600: rgb(var(--primary-600));
--color-purple-700: rgb(var(--primary-700));
--color-purple-800: rgb(var(--primary-800));
--color-purple-900: rgb(var(--primary-900));
--color-purple: rgb(var(--primary-500));
--color-purple-foreground: rgb(var(--primary-foreground));
--color-blue-50: rgb(var(--secondary-50));
--color-blue-100: rgb(var(--secondary-100));
--color-blue-200: rgb(var(--secondary-200));
--color-blue-300: rgb(var(--secondary-300));
--color-blue-400: rgb(var(--secondary-400));
--color-blue-500: rgb(var(--secondary-500));
--color-blue-600: rgb(var(--secondary-600));
--color-blue-700: rgb(var(--secondary-700));
--color-blue-800: rgb(var(--secondary-800));
--color-blue-900: rgb(var(--secondary-900));
--color-blue: rgb(var(--secondary-500));
--color-blue-foreground: rgb(var(--secondary-foreground));
--color-yellow-50: rgb(var(--warning-50));
--color-yellow-100: rgb(var(--warning-100));
--color-yellow-200: rgb(var(--warning-200));
--color-yellow-300: rgb(var(--warning-300));
--color-yellow-400: rgb(var(--warning-400));
--color-yellow-500: rgb(var(--warning-500));
--color-yellow-600: rgb(var(--warning-600));
--color-yellow-700: rgb(var(--warning-700));
--color-yellow-800: rgb(var(--warning-800));
--color-yellow-900: rgb(var(--warning-900));
--color-yellow: rgb(var(--warning-500));
--color-yellow-foreground: rgb(var(--warning-foreground));
--color-red-50: rgb(var(--error-50));
--color-red-100: rgb(var(--error-100));
--color-red-200: rgb(var(--error-200));
--color-red-300: rgb(var(--error-300));
--color-red-400: rgb(var(--error-400));
--color-red-500: rgb(var(--error-500));
--color-red-600: rgb(var(--error-600));
--color-red-700: rgb(var(--error-700));
--color-red-800: rgb(var(--error-800));
--color-red-900: rgb(var(--error-900));
--color-red: rgb(var(--error-500));
--color-red-foreground: rgb(var(--error-foreground));
--color-primary-50: rgb(var(--primary-50));
--color-primary-100: rgb(var(--primary-100));
--color-primary-200: rgb(var(--primary-200));
--color-primary-300: rgb(var(--primary-300));
--color-primary-400: rgb(var(--primary-400));
--color-primary-500: rgb(var(--primary-500));
--color-primary-600: rgb(var(--primary-600));
--color-primary-700: rgb(var(--primary-700));
--color-primary-800: rgb(var(--primary-800));
--color-primary-900: rgb(var(--primary-900));
--color-primary: rgb(var(--primary-500));
--color-primary-foreground: rgb(var(--primary-foreground));
--color-secondary-50: rgb(var(--secondary-50));
--color-secondary-100: rgb(var(--secondary-100));
--color-secondary-200: rgb(var(--secondary-200));
--color-secondary-300: rgb(var(--secondary-300));
--color-secondary-400: rgb(var(--secondary-400));
--color-secondary-500: rgb(var(--secondary-500));
--color-secondary-600: rgb(var(--secondary-600));
--color-secondary-700: rgb(var(--secondary-700));
--color-secondary-800: rgb(var(--secondary-800));
--color-secondary-900: rgb(var(--secondary-900));
--color-secondary: rgb(var(--secondary-500));
--color-secondary-foreground: rgb(var(--secondary-foreground));
--color-tertiary-50: rgb(var(--tertiary-50));
--color-tertiary-100: rgb(var(--tertiary-100));
--color-tertiary-200: rgb(var(--tertiary-200));
--color-tertiary-300: rgb(var(--tertiary-300));
--color-tertiary-400: rgb(var(--tertiary-400));
--color-tertiary-500: rgb(var(--tertiary-500));
--color-tertiary-600: rgb(var(--tertiary-600));
--color-tertiary-700: rgb(var(--tertiary-700));
--color-tertiary-800: rgb(var(--tertiary-800));
--color-tertiary-900: rgb(var(--tertiary-900));
--color-tertiary: rgb(var(--tertiary-500));
--color-tertiary-foreground: rgb(var(--tertiary-foreground));
}
@layer base {
/* Preserve v3 Default Border Color - v4 changed the unqualified `border` utility to currentColor. */
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: rgb(var(--border));
}
html,
body {
overscroll-behavior-y: none;
+2 -2
View File
@@ -70,7 +70,7 @@ export default function AdminImportPage() {
if (selectedDirectory) {
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
<div className="flex grow flex-col gap-2 rounded bg-surface p-4 text-content-muted shadow-lg">
<p className="text-lg font-semibold text-content">Selected Import Directory</p>
<form className="flex flex-col gap-4" onSubmit={handleImport}>
@@ -122,7 +122,7 @@ export default function AdminImportPage() {
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
<table className="min-w-full bg-surface text-sm leading-normal text-content">
<thead className="text-content-muted">
<tr>
@@ -13,19 +13,15 @@ export default function AdminImportResultsPage() {
return (
<div className="overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
<table className="min-w-full bg-surface text-sm leading-normal text-content">
<thead className="text-content-muted">
<tr>
<th className="border-b border-border p-3 text-left font-normal uppercase">
Document
</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">
Status
</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">
Error
</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">Status</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">Error</th>
</tr>
</thead>
<tbody>
@@ -44,7 +40,10 @@ export default function AdminImportResultsPage() {
>
<span className="text-content-muted">Name:</span>
{result.id ? (
<Link to={`/documents/${result.id}`} className="text-secondary-600 hover:underline">
<Link
to={`/documents/${result.id}`}
className="text-secondary-600 hover:underline"
>
{result.name}
</Link>
) : (
+2 -2
View File
@@ -30,14 +30,14 @@ export default function AdminLogsPage() {
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleFilterSubmit}>
<div className="flex w-full grow flex-col">
<div className="relative flex">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
<Search2Icon size={15} hoverable={false} />
</span>
<input
type="text"
value={filter}
onChange={e => setFilter(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface p-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface p-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="JQ Filter"
/>
</div>
+4 -2
View File
@@ -153,7 +153,7 @@ export default function AdminUsersPage() {
</div>
)}
<div className="min-w-full overflow-scroll rounded shadow">
<div className="min-w-full overflow-scroll rounded shadow-sm">
<table className="min-w-full bg-surface text-sm leading-normal text-content">
<thead className="text-content-muted">
<tr>
@@ -163,7 +163,9 @@ export default function AdminUsersPage() {
</button>
</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">User</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">Password</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">
Password
</th>
<th className="border-b border-border p-3 text-center font-normal uppercase">
Permissions
</th>
+6 -6
View File
@@ -41,7 +41,7 @@ export default function ComponentDemoPage() {
<div className="space-y-8 p-4 text-content">
<h1 className="text-2xl font-bold">UI Components Demo</h1>
<section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-surface p-6 shadow-sm">
<h2 className="mb-4 text-xl font-semibold">Toast Notifications</h2>
<div className="flex flex-wrap gap-4">
<button
@@ -72,7 +72,7 @@ export default function ComponentDemoPage() {
</div>
</section>
<section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-surface p-6 shadow-sm">
<h2 className="mb-4 text-xl font-semibold">Skeleton Loading Components</h2>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
@@ -115,7 +115,7 @@ export default function ComponentDemoPage() {
</div>
</section>
<section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-surface p-6 shadow-sm">
<h2 className="mb-4 text-xl font-semibold">Skeleton Cards</h2>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<SkeletonCard />
@@ -124,17 +124,17 @@ export default function ComponentDemoPage() {
</div>
</section>
<section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-surface p-6 shadow-sm">
<h2 className="mb-4 text-xl font-semibold">Skeleton Table</h2>
<SkeletonTable rows={5} columns={4} />
</section>
<section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-surface p-6 shadow-sm">
<h2 className="mb-4 text-xl font-semibold">Page Loader</h2>
<PageLoader message="Loading demo content..." />
</section>
<section className="rounded-lg bg-surface p-6 shadow">
<section className="rounded-lg bg-surface p-6 shadow-sm">
<h2 className="mb-4 text-xl font-semibold">Inline Loader</h2>
<div className="flex items-center gap-8">
<div className="text-center">
+75 -17
View File
@@ -21,10 +21,11 @@ import {
import { Field, FieldLabel, FieldValue, FieldActions } from '../components';
const iconButtonClassName = 'cursor-pointer text-content-muted hover:text-content';
const popupClassName = 'rounded bg-surface-strong p-3 text-content shadow-lg transition-all duration-200';
const popupClassName =
'rounded bg-surface-strong p-3 text-content shadow-lg transition-all duration-200';
const popupInputClassName = 'rounded bg-surface p-2 text-content';
const editInputClassName =
'w-full rounded border border-secondary-200 bg-secondary-50 p-2 text-lg font-medium text-content focus:outline-none focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500';
'w-full rounded border border-secondary-200 bg-secondary-50 p-2 text-lg font-medium text-content focus:outline-hidden focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500';
export default function DocumentPage() {
const { id } = useParams<{ id: string }>();
@@ -122,7 +123,7 @@ export default function DocumentPage() {
{document.filepath && (
<a
href={`/reader/${document.id}`}
className="z-10 mt-2 w-full rounded bg-secondary-700 py-1 text-center text-sm font-medium text-white hover:bg-secondary-800 focus:outline-none focus:ring-4 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700"
className="z-10 mt-2 w-full rounded bg-secondary-700 py-1 text-center text-sm font-medium text-white hover:bg-secondary-800 focus:outline-hidden focus:ring-4 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700"
>
Read
</a>
@@ -154,7 +155,12 @@ export default function DocumentPage() {
}`}
>
<form className="flex w-72 flex-col gap-2 text-sm">
<input type="file" id="cover_file" name="cover_file" className={popupInputClassName} />
<input
type="file"
id="cover_file"
name="cover_file"
className={popupInputClassName}
/>
<button
type="submit"
className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
@@ -163,7 +169,13 @@ export default function DocumentPage() {
</button>
</form>
<form className="flex w-72 flex-col gap-2 text-sm">
<input type="checkbox" checked id="remove_cover" name="remove_cover" className="hidden" />
<input
type="checkbox"
checked
id="remove_cover"
name="remove_cover"
className="hidden"
/>
<button
type="submit"
className="rounded bg-secondary-700 px-2 py-1 text-sm font-medium text-white hover:bg-secondary-800 dark:bg-secondary-600"
@@ -283,10 +295,20 @@ export default function DocumentPage() {
<FieldActions>
{isEditingTitle ? (
<div className="flex gap-1">
<button type="button" onClick={() => setIsEditingTitle(false)} className={iconButtonClassName} aria-label="Cancel edit">
<button
type="button"
onClick={() => setIsEditingTitle(false)}
className={iconButtonClassName}
aria-label="Cancel edit"
>
<CloseIcon size={18} />
</button>
<button type="button" onClick={saveTitle} className={iconButtonClassName} aria-label="Confirm edit">
<button
type="button"
onClick={saveTitle}
className={iconButtonClassName}
aria-label="Confirm edit"
>
<CheckIcon size={18} />
</button>
</div>
@@ -309,7 +331,12 @@ export default function DocumentPage() {
>
{isEditingTitle ? (
<div className="relative mt-1 flex gap-2">
<input type="text" value={editTitle} onChange={e => setEditTitle(e.target.value)} className={editInputClassName} />
<input
type="text"
value={editTitle}
onChange={e => setEditTitle(e.target.value)}
className={editInputClassName}
/>
</div>
) : (
<FieldValue>{document.title}</FieldValue>
@@ -324,10 +351,20 @@ export default function DocumentPage() {
<FieldActions>
{isEditingAuthor ? (
<>
<button type="button" onClick={() => setIsEditingAuthor(false)} className={iconButtonClassName} aria-label="Cancel edit">
<button
type="button"
onClick={() => setIsEditingAuthor(false)}
className={iconButtonClassName}
aria-label="Cancel edit"
>
<CloseIcon size={18} />
</button>
<button type="button" onClick={saveAuthor} className={iconButtonClassName} aria-label="Confirm edit">
<button
type="button"
onClick={saveAuthor}
className={iconButtonClassName}
aria-label="Confirm edit"
>
<CheckIcon size={18} />
</button>
</>
@@ -350,7 +387,12 @@ export default function DocumentPage() {
>
{isEditingAuthor ? (
<div className="relative mt-1 flex gap-2">
<input type="text" value={editAuthor} onChange={e => setEditAuthor(e.target.value)} className={editInputClassName} />
<input
type="text"
value={editAuthor}
onChange={e => setEditAuthor(e.target.value)}
className={editInputClassName}
/>
</div>
) : (
<FieldValue>{document.author}</FieldValue>
@@ -376,11 +418,15 @@ export default function DocumentPage() {
>
<div className="flex text-xs">
<p className="w-32 text-content-subtle">Seconds / Percent</p>
<p className="font-medium">{secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'}</p>
<p className="font-medium">
{secondsPerPercent !== 0 ? secondsPerPercent : 'N/A'}
</p>
</div>
<div className="flex text-xs">
<p className="w-32 text-content-subtle">Words / Minute</p>
<p className="font-medium">{document.wpm && document.wpm > 0 ? document.wpm : 'N/A'}</p>
<p className="font-medium">
{document.wpm && document.wpm > 0 ? document.wpm : 'N/A'}
</p>
</div>
<div className="flex text-xs">
<p className="w-32 text-content-subtle">Est. Time Left</p>
@@ -412,10 +458,20 @@ export default function DocumentPage() {
<FieldActions>
{isEditingDescription ? (
<>
<button type="button" onClick={() => setIsEditingDescription(false)} className={iconButtonClassName} aria-label="Cancel edit">
<button
type="button"
onClick={() => setIsEditingDescription(false)}
className={iconButtonClassName}
aria-label="Cancel edit"
>
<CloseIcon size={18} />
</button>
<button type="button" onClick={saveDescription} className={iconButtonClassName} aria-label="Confirm edit">
<button
type="button"
onClick={saveDescription}
className={iconButtonClassName}
aria-label="Confirm edit"
>
<CheckIcon size={18} />
</button>
</>
@@ -441,12 +497,14 @@ export default function DocumentPage() {
<textarea
value={editDescription}
onChange={e => setEditDescription(e.target.value)}
className="h-32 w-full grow rounded border border-secondary-200 bg-secondary-50 p-2 font-medium text-content focus:outline-none focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500"
className="h-32 w-full grow rounded border border-secondary-200 bg-secondary-50 p-2 font-medium text-content focus:outline-hidden focus:ring-2 focus:ring-secondary-400 dark:border-secondary-700 dark:bg-secondary-900/20 dark:focus:ring-secondary-500"
rows={5}
/>
</div>
) : (
<FieldValue className="hyphens-auto text-justify">{document.description || 'N/A'}</FieldValue>
<FieldValue className="hyphens-auto text-justify">
{document.description || 'N/A'}
</FieldValue>
)}
</Field>
</div>
+4 -4
View File
@@ -28,7 +28,7 @@ function DocumentCard({ doc }: DocumentCardProps) {
<div
role="link"
tabIndex={0}
className="flex size-full cursor-pointer gap-4 rounded bg-surface p-4 shadow-lg transition-colors hover:bg-surface-muted focus:outline-none"
className="flex size-full cursor-pointer gap-4 rounded bg-surface p-4 shadow-lg transition-colors hover:bg-surface-muted focus:outline-hidden"
onClick={() => navigate(`/documents/${doc.id}`)}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === ' ') {
@@ -100,7 +100,7 @@ function DocumentListItem({ doc }: DocumentListItemProps) {
<div
role="link"
tabIndex={0}
className="block cursor-pointer rounded bg-surface p-4 text-content shadow-lg transition-colors hover:bg-surface-muted focus:outline-none"
className="block cursor-pointer rounded bg-surface p-4 text-content shadow-lg transition-colors hover:bg-surface-muted focus:outline-hidden"
onClick={() => navigate(`/documents/${doc.id}`)}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === ' ') {
@@ -214,14 +214,14 @@ export default function DocumentsPage() {
<div className="flex flex-col gap-4 lg:flex-row">
<div className="flex w-full grow flex-col">
<div className="relative flex">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
<Search2Icon size={15} hoverable={false} />
</span>
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="Search Author / Title"
name="search"
/>
+3 -3
View File
@@ -56,7 +56,7 @@ export function LoginPageView({
type="text"
value={username}
onChange={e => onUsernameChange(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="Username"
required
disabled={isLoading}
@@ -69,7 +69,7 @@ export function LoginPageView({
type="password"
value={password}
onChange={e => onPasswordChange(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="Password"
required
disabled={isLoading}
@@ -80,7 +80,7 @@ export function LoginPageView({
variant="secondary"
type="submit"
disabled={isLoading}
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-none focus:ring-2 disabled:opacity-50"
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-hidden focus:ring-2 disabled:opacity-50"
>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
+11 -5
View File
@@ -74,7 +74,10 @@ export default function ReaderPage() {
colorScheme,
fontFamily,
fontSize,
isPaginationDisabled: useCallback(() => isTopBarOpen || isBottomBarOpen, [isTopBarOpen, isBottomBarOpen]),
isPaginationDisabled: useCallback(
() => isTopBarOpen || isBottomBarOpen,
[isTopBarOpen, isBottomBarOpen]
),
onSwipeDown: handleSwipeDown,
onSwipeUp: handleSwipeUp,
onCenterTap: handleCenterTap,
@@ -86,9 +89,10 @@ export default function ReaderPage() {
}
}, [document?.title]);
const { setTheme } = reader;
useEffect(() => {
reader.setTheme({ colorScheme, fontFamily, fontSize });
}, [colorScheme, fontFamily, fontSize, reader.setTheme]);
setTheme({ colorScheme, fontFamily, fontSize });
}, [colorScheme, fontFamily, fontSize, setTheme]);
useEffect(() => {
if (isTopBarOpen || isBottomBarOpen) {
@@ -122,7 +126,7 @@ export default function ReaderPage() {
<div className="flex min-w-0 items-start gap-4">
<Link to={`/documents/${document.id}`} className="block shrink-0">
<img
className="h-28 w-20 rounded object-cover shadow"
className="h-28 w-20 rounded object-cover shadow-sm"
src={`/api/v1/documents/${document.id}/cover`}
alt={`${document.title} cover`}
/>
@@ -216,7 +220,9 @@ export default function ReaderPage() {
<div className="grid gap-3 lg:grid-cols-[minmax(0,2fr)_minmax(0,2fr)_auto] lg:items-start">
<div className="min-w-0">
<p className="mb-1 text-[10px] uppercase tracking-wide text-content-subtle">Theme</p>
<p className="mb-1 text-[10px] uppercase tracking-wide text-content-subtle">
Theme
</p>
<div className="grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 lg:grid-cols-5">
{colorSchemes.map(option => (
<button
+3 -3
View File
@@ -61,7 +61,7 @@ export default function RegisterPage() {
type="text"
value={username}
onChange={e => setUsername(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="Username"
required
disabled={isLoading || isLoadingInfo || !registrationEnabled}
@@ -74,7 +74,7 @@ export default function RegisterPage() {
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="Password"
required
disabled={isLoading || isLoadingInfo || !registrationEnabled}
@@ -85,7 +85,7 @@ export default function RegisterPage() {
variant="secondary"
type="submit"
disabled={isLoading || isLoadingInfo || !registrationEnabled}
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-none focus:ring-2 disabled:opacity-50"
className="w-full px-4 py-2 text-center text-base font-semibold transition duration-200 ease-in focus:outline-hidden focus:ring-2 disabled:opacity-50"
>
{isLoading ? 'Registering...' : 'Register'}
</Button>
+7 -11
View File
@@ -53,26 +53,26 @@ export function SearchPageView({
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={onSubmit}>
<div className="flex w-full grow flex-col">
<div className="relative flex">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
<Search2Icon size={15} hoverable={false} />
</span>
<input
type="text"
value={query}
onChange={e => onQueryChange(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="Query"
/>
</div>
</div>
<div className="relative flex min-w-[12em]">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
<BookIcon size={15} />
</span>
<select
value={source}
onChange={e => onSourceChange(e.target.value as GetSearchSource)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
>
<option value={GetSearchSource.LibGen}>Library Genesis</option>
<option value={GetSearchSource.Annas_Archive}>Annas Archive</option>
@@ -86,7 +86,7 @@ export function SearchPageView({
</form>
</div>
<div className="inline-block min-w-full overflow-hidden rounded shadow">
<div className="inline-block min-w-full overflow-hidden rounded shadow-sm">
<table className="min-w-full bg-surface text-sm leading-normal text-content md:text-sm">
<thead className="text-content-muted">
<tr>
@@ -97,12 +97,8 @@ export function SearchPageView({
<th className="border-b border-border p-3 text-left font-normal uppercase">
Series
</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">
Type
</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">
Size
</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">Type</th>
<th className="border-b border-border p-3 text-left font-normal uppercase">Size</th>
<th className="hidden border-b border-border p-3 text-left font-normal uppercase md:table-cell">
Date
</th>
+6 -6
View File
@@ -143,28 +143,28 @@ export default function SettingsPage() {
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handlePasswordSubmit}>
<div className="flex grow flex-col">
<div className="relative flex">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
<PasswordIcon size={15} />
</span>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="Password"
/>
</div>
</div>
<div className="flex grow flex-col">
<div className="relative flex">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
<PasswordIcon size={15} />
</span>
<input
type="password"
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
placeholder="New Password"
/>
</div>
@@ -220,13 +220,13 @@ export default function SettingsPage() {
<p className="mb-2 text-lg font-semibold text-content">Change Timezone</p>
<form className="flex flex-col gap-4 lg:flex-row" onSubmit={handleTimezoneSubmit}>
<div className="relative flex grow">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-sm">
<span className="inline-flex items-center border-y border-l border-border bg-surface px-3 text-sm text-content-muted shadow-xs">
<ClockIcon size={15} />
</span>
<select
value={timezone || 'UTC'}
onChange={e => setTimezone(e.target.value)}
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-sm placeholder:text-content-subtle focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600"
className="w-full flex-1 appearance-none rounded-none border border-border bg-surface px-4 py-2 text-base text-content shadow-xs placeholder:text-content-subtle focus:border-transparent focus:outline-hidden focus:ring-2 focus:ring-primary-600"
>
<option value="UTC">UTC</option>
<option value="America/New_York">America/New_York</option>
-51
View File
@@ -1,51 +0,0 @@
const withOpacity = cssVariable => `rgb(var(${cssVariable}) / <alpha-value>)`;
const buildScale = scaleName => ({
50: withOpacity(`--${scaleName}-50`),
100: withOpacity(`--${scaleName}-100`),
200: withOpacity(`--${scaleName}-200`),
300: withOpacity(`--${scaleName}-300`),
400: withOpacity(`--${scaleName}-400`),
500: withOpacity(`--${scaleName}-500`),
600: withOpacity(`--${scaleName}-600`),
700: withOpacity(`--${scaleName}-700`),
800: withOpacity(`--${scaleName}-800`),
900: withOpacity(`--${scaleName}-900`),
DEFAULT: withOpacity(`--${scaleName}-500`),
foreground: withOpacity(`--${scaleName}-foreground`),
});
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
theme: {
extend: {
colors: {
canvas: withOpacity('--canvas'),
surface: withOpacity('--surface'),
'surface-muted': withOpacity('--surface-muted'),
'surface-strong': withOpacity('--surface-strong'),
overlay: withOpacity('--overlay'),
content: withOpacity('--content'),
'content-muted': withOpacity('--content-muted'),
'content-subtle': withOpacity('--content-subtle'),
'content-inverse': withOpacity('--content-inverse'),
border: withOpacity('--border'),
'border-muted': withOpacity('--border-muted'),
'border-strong': withOpacity('--border-strong'),
white: withOpacity('--white'),
black: withOpacity('--black'),
gray: buildScale('neutral'),
purple: buildScale('primary'),
blue: buildScale('secondary'),
yellow: buildScale('warning'),
red: buildScale('error'),
primary: buildScale('primary'),
secondary: buildScale('secondary'),
tertiary: buildScale('tertiary'),
},
},
},
plugins: [],
};
+2 -1
View File
@@ -1,8 +1,9 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [react()],
plugins: [react(), tailwindcss()],
server: {
allowedHosts: ['lin-va-terminal'],
proxy: {