diff --git a/.github/workflows/test-app.yml b/.github/workflows/test-app.yml index af4773cd..20f8bd78 100644 --- a/.github/workflows/test-app.yml +++ b/.github/workflows/test-app.yml @@ -21,7 +21,7 @@ jobs: run: npm ci - name: Lint - run: npm run lint + run: npx biome ci test: name: Test diff --git a/CLAUDE.md b/CLAUDE.md index 7b0641d6..8455e504 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,8 @@ From repo root (delegates to `apps/hudozka`): - `npm run dev` — dev server with Turbo (`next dev --turbo`) - `npm run build` — production build -- `npm run lint` — ESLint across the whole monorepo -- `npm run format` — auto-fix lint issues +- `npm run lint` — Biome lint + format check across the whole monorepo (`biome check`) +- `npm run format` — auto-fix lint and format issues (`biome check --write`) - `npm test` — run unit tests (Vitest) across every workspace Per-app: @@ -35,7 +35,7 @@ Feature branches are named `issue-XXX` where `XXX` is the related GitHub issue n ## Code Style -ESLint 9 flat config (`eslint.config.js`) with native `eslint-config-next` 16.x support. Extends `next/core-web-vitals`. Enforces: no semicolons, double quotes, 4-space indentation, always-multiline trailing commas, `eol-last`. +Biome (`biome.json`) handles formatting and linting. Style: no semicolons, double quotes, 4-space indentation, always-multiline trailing commas, LF line endings, 120-column line width. Lint scope is limited to `**/src/**/*.{ts,tsx,js}` via `files.includes`. Console calls are restricted to `console.error`/`console.warn` (full `console.*` is allowed only inside `apps/migrate-to-pocketbase`). `noDangerouslySetInnerHtml` is disabled because the site renders trusted CMS HTML. Inline `biome-ignore` / `eslint-disable` comments are not used — fix the underlying issue instead. **React components:** Use regular function declarations (not arrow functions) and `type` for props. Do not use `React.FC`. diff --git a/apps/hudozka-writer/src/App.tsx b/apps/hudozka-writer/src/App.tsx index c5b53d71..b34cdc18 100644 --- a/apps/hudozka-writer/src/App.tsx +++ b/apps/hudozka-writer/src/App.tsx @@ -1,7 +1,7 @@ -import { useState, useEffect, useCallback } from "react" -import { pb } from "./pb" -import { Login } from "./components/Login" +import { useCallback, useEffect, useState } from "react" import { Editor } from "./components/Editor" +import { Login } from "./components/Login" +import { pb } from "./pb" import type { PbPage } from "./types" import "./App.css" @@ -22,9 +22,7 @@ export function App() { setLoading(true) setError("") try { - const record = await pb.collection("pages").getFirstListItem( - `slug="${slug}"`, - ) + const record = await pb.collection("pages").getFirstListItem(`slug="${slug}"`) setPage(record) } catch { setError(`Page "${slug}" not found`) @@ -87,8 +85,12 @@ export function App() { if (!slug) { return (
-

Add ?page=slug to the URL to edit a page.

- +

+ Add ?page=slug to the URL to edit a page. +

+
) } @@ -101,7 +103,9 @@ export function App() { return (

{error}

- +
) } @@ -116,15 +120,15 @@ export function App() { {page.title} /{page.slug} - - + +
diff --git a/apps/hudozka-writer/src/components/BlockActions.tsx b/apps/hudozka-writer/src/components/BlockActions.tsx index 8fdeac43..b02b9442 100644 --- a/apps/hudozka-writer/src/components/BlockActions.tsx +++ b/apps/hudozka-writer/src/components/BlockActions.tsx @@ -1,6 +1,6 @@ -import { useEffect, useCallback } from "react" -import type { Editor } from "@tiptap/react" import { Fragment } from "@tiptap/pm/model" +import type { Editor } from "@tiptap/react" +import { useCallback, useEffect } from "react" import "./BlockActions.css" export type BlockActionsProps = { @@ -68,10 +68,14 @@ export function BlockActions({ editor, getPos, showSplit }: BlockActionsProps) { if (typeof pos !== "number") return const node = editor.state.doc.nodeAt(pos) if (!node) return - editor.chain().focus().deleteRange({ - from: pos, - to: pos + node.nodeSize, - }).run() + editor + .chain() + .focus() + .deleteRange({ + from: pos, + to: pos + node.nodeSize, + }) + .run() } const handleSplit = (e: React.MouseEvent) => { @@ -120,6 +124,7 @@ export function BlockActions({ editor, getPos, showSplit }: BlockActionsProps) { return (
{menuOpen && (
- +
)}
diff --git a/apps/hudozka-writer/src/components/BlockMenu.tsx b/apps/hudozka-writer/src/components/BlockMenu.tsx index ba716801..61d2b73e 100644 --- a/apps/hudozka-writer/src/components/BlockMenu.tsx +++ b/apps/hudozka-writer/src/components/BlockMenu.tsx @@ -25,21 +25,29 @@ export type BlockMenuProps = { export function BlockMenu({ editor, position, onClose, filter }: BlockMenuProps) { const filtered = filter - ? BLOCK_TYPES.filter((b) => b.label.toLowerCase().includes(filter.toLowerCase())) + ? BLOCK_TYPES.filter(b => b.label.toLowerCase().includes(filter.toLowerCase())) : BLOCK_TYPES function handleInsert(block: BlockType) { const id = generateBlockId() if (block.type === "textBlock") { - editor.chain().focus().insertContentAt(position, { - type: "textBlock", - content: [{ type: "paragraph" }], - }).run() + editor + .chain() + .focus() + .insertContentAt(position, { + type: "textBlock", + content: [{ type: "paragraph" }], + }) + .run() } else { - editor.chain().focus().insertContentAt(position, { - type: block.type, - attrs: { ...block.attrs, id }, - }).run() + editor + .chain() + .focus() + .insertContentAt(position, { + type: block.type, + attrs: { ...block.attrs, id }, + }) + .run() } onClose() } @@ -48,12 +56,8 @@ export function BlockMenu({ editor, position, onClose, filter }: BlockMenuProps) return (
- {filtered.map((block) => ( - ))} @@ -61,5 +65,5 @@ export function BlockMenu({ editor, position, onClose, filter }: BlockMenuProps) ) } -export { BLOCK_TYPES } export type { BlockType } +export { BLOCK_TYPES } diff --git a/apps/hudozka-writer/src/components/DraftBanner.tsx b/apps/hudozka-writer/src/components/DraftBanner.tsx index c1674d5e..868b0083 100644 --- a/apps/hudozka-writer/src/components/DraftBanner.tsx +++ b/apps/hudozka-writer/src/components/DraftBanner.tsx @@ -8,7 +8,7 @@ export function DraftBanner({ onDiscard }: DraftBannerProps) { return (
You have unpublished changes -
diff --git a/apps/hudozka-writer/src/components/Editor.tsx b/apps/hudozka-writer/src/components/Editor.tsx index 1f846ff0..b6473f88 100644 --- a/apps/hudozka-writer/src/components/Editor.tsx +++ b/apps/hudozka-writer/src/components/Editor.tsx @@ -1,24 +1,17 @@ -import { useEditor, EditorContent } from "@tiptap/react" -import StarterKit from "@tiptap/starter-kit" -import { Markdown } from "@tiptap/markdown" import Link from "@tiptap/extension-link" -import { useState, useCallback, useRef, useEffect } from "react" -import { pb } from "../pb" -import { docToTiptap, tiptapToDoc } from "../lib/serialize" -import { Toolbar } from "./Toolbar" -import { DraftBanner } from "./DraftBanner" -import { - WriterDocument, - TextBlock, - ImageBlock, - DocumentBlock, - EmbedBlock, - CardGridBlock, -} from "../nodes" +import { Markdown } from "@tiptap/markdown" +import { EditorContent, useEditor } from "@tiptap/react" +import StarterKit from "@tiptap/starter-kit" +import { useCallback, useRef, useState } from "react" import { SlashCommands } from "../extensions/SlashCommands" +import { docToTiptap, tiptapToDoc } from "../lib/serialize" +import { CardGridBlock, DocumentBlock, EmbedBlock, ImageBlock, TextBlock, WriterDocument } from "../nodes" +import { pb } from "../pb" +import { DraftBanner } from "./DraftBanner" +import { Toolbar } from "./Toolbar" import "../nodes/nodes.css" import "./Editor.css" -import type { PbPage, DocV1 } from "../types" +import type { DocV1, PbPage } from "../types" function serializeDoc(doc: DocV1): string { return JSON.stringify(doc) @@ -37,15 +30,10 @@ export function Editor({ page }: EditorProps) { const [hasDraft, setHasDraft] = useState(page.draft !== null) const [publishedDoc, setPublishedDoc] = useState(page.doc) - const snapshotRef = useRef("") - const initialDoc = page.draft ?? page.doc const tiptapDoc = docToTiptap(initialDoc) - // Set initial snapshot - useEffect(() => { - snapshotRef.current = serializeDoc(initialDoc) - }, []) // eslint-disable-line react-hooks/exhaustive-deps + const snapshotRef = useRef(serializeDoc(initialDoc)) const editor = useEditor({ extensions: [ @@ -157,14 +145,12 @@ export function Editor({ page }: EditorProps) { markdownMode={markdownMode} onToggleMarkdown={handleToggleMarkdown} /> - {hasDraft && ( - - )} + {hasDraft && } {markdownMode ? (