Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/oxlint-biome-split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-slide/core": patch
---

Fix lint findings uncovered by Oxlint (floating promises, hook deps, a11y labels).
49 changes: 49 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "react", "import", "vitest", "jsx-a11y"],
"categories": {
"correctness": "error"
},
"env": {
"builtin": true
},
"settings": {
"react": {
"version": "19.2.7"
}
},
"ignorePatterns": [
"**/dist/**",
"**/node_modules/**",
"**/*.tsbuildinfo",
"packages/core/src/app/components/ui/**",
"packages/cli/template/**"
],
"rules": {
"import/no-cycle": "error",
"typescript/no-floating-promises": "error",
"jsx-a11y/prefer-tag-over-role": "off",
"vitest/expect-expect": [
"error",
{
"assertFunctionNames": [
"expect",
"expectTypeOf",
"assert",
"expectTagged",
"expectTaggedTransform"
]
}
]
},
"overrides": [
{
"files": ["**/slides/**", "apps/web/components/landing/**"],
"rules": {
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"jsx-a11y/prefer-tag-over-role": "off"
}
}
]
}
10 changes: 7 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ Shared config: `biome.json`, `turbo.json`, `pnpm-workspace.yaml`, `tsconfig` per
pnpm dev # turbo: runs demo against local core
pnpm build # build all packages
pnpm typecheck # tsc across the graph
pnpm check # biome (format + lint + organize imports)
pnpm check:fix # auto-fix what biome can
pnpm format:check # biome formatter
pnpm lint # oxlint (JS/TS) + biome (CSS/JSON)
pnpm lint:js # oxlint only
pnpm lint:nonjs # biome non-JS lint only
pnpm check # format:check + lint + typecheck
pnpm check:fix # oxlint --fix + biome check --write
pnpm test # vitest
```

Filter to one package: `pnpm core <script>` / `pnpm cli <script>`.

## Hard rules

- **Biome must pass before commit.** Run `pnpm check` (or `pnpm check:fix`). CI and the user's review both expect a clean tree.
- **Format and lint must pass before commit.** Run `pnpm check` (or `pnpm check:fix`). Oxlint owns JS/TS semantics; Biome owns formatting and non-JS lint. CI and the user's review both expect a clean tree.
- **If `packages/core` or `packages/cli` changes, add a changeset.** Run `pnpm changeset`, pick the right package(s) and bump (`patch` for fixes/polish, `minor` for new public API, `major` for breaking). Apps (`demo`, `web`) and root tooling do **not** need one.
- **Changeset descriptions: short and direct.** One line, present-tense, what changed from a user's perspective. Match the tone of `.changeset/*.md` already in the repo. No paragraphs, no rationale, no "this PR…".
- Good: `Replace spinner with a hairline + sliding bar for slide and presenter loading states.`
Expand Down
13 changes: 7 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ pnpm dev
pnpm dev # turbo: runs demo against local core
pnpm build # build all packages
pnpm typecheck # tsc across the graph
pnpm check # biome (format + lint + organize imports)
pnpm check:fix # auto-fix what biome can
pnpm format:check # biome formatter
pnpm lint # oxlint (JS/TS) + biome (CSS/JSON)
pnpm check # format:check + lint + typecheck
pnpm check:fix # oxlint --fix + biome check --write
pnpm test # vitest
```

Expand All @@ -70,8 +72,7 @@ pnpm cli <script>
2. **Make your change.** Match the surrounding style. Don't reformat unrelated code.
3. **Run the checks before pushing:**
```bash
pnpm check # must pass — CI enforces it
pnpm typecheck
pnpm check # must pass — CI enforces format + lint + typecheck
pnpm test
```
`pnpm check:fix` will auto-fix most formatting and lint issues.
Expand All @@ -98,10 +99,10 @@ pnpm cli <script>

## Style & conventions

- **Biome must pass.** Formatting, lint, and import organisation are all enforced by `pnpm check`.
- **Format and lint must pass.** Formatting and non-JS lint are enforced by Biome; JS/TS semantic lint is enforced by Oxlint. Use `pnpm check`.
- **No casual dependencies.** The `core` runtime ships to users — every dep inflates install size. Prefer a small piece of inline code over a new package.
- **Default to writing no comments.** Only add one when the *why* is non-obvious — a hidden constraint, a subtle invariant, a workaround for a specific bug. Don't explain *what* the code does; well-named identifiers handle that.
- **Leave `packages/core/src/app/components/ui` alone.** It's shadcn-generated and biome-ignored unless you're regenerating it.
- **Leave `packages/core/src/app/components/ui` alone.** It's shadcn-generated and ignored by Biome/Oxlint unless you're regenerating it.

## Testing

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ This repo is a pnpm + Turbo monorepo.
pnpm install
pnpm dev # runs the demo against the local @open-slide/core
pnpm build # builds all packages
pnpm check # type-checks all packages
pnpm lint # lints via biome
pnpm check # format:check + lint + typecheck
pnpm lint # oxlint (JS/TS) + biome (CSS/JSON)
```

## Star history
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/slides/open-slide-launch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const Letters = ({
style?: CSSProperties;
}) => (
<span className={className} style={{ display: 'inline-flex', whiteSpace: 'pre', ...style }}>
{[...text].map((c, i) => (
{Array.from(text).map((c, i) => (
<span
key={i}
style={{
Expand Down
1 change: 1 addition & 0 deletions apps/web/components/landing/hero-setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function HeroSetup() {
) : null}
<button
type="button"
aria-label={setupOptions[key].label}
aria-pressed={mode === key}
onClick={() => selectMode(key)}
className={`pressable rounded-md py-1.5 ${
Expand Down
8 changes: 4 additions & 4 deletions apps/web/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@ import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation';
import { type NextRequest, NextResponse } from 'next/server';
import { docsContentRoute, docsRoute } from '@/lib/shared';

const { rewrite: rewriteDocs } = rewritePath(
const docsPathRewrite = rewritePath(
`${docsRoute}{/*path}`,
`${docsContentRoute}{/*path}/content.md`,
);
const { rewrite: rewriteSuffix } = rewritePath(
const suffixPathRewrite = rewritePath(
`${docsRoute}{/*path}.mdx`,
`${docsContentRoute}{/*path}/content.md`,
);

export default function proxy(request: NextRequest) {
const result = rewriteSuffix(request.nextUrl.pathname);
const result = suffixPathRewrite.rewrite(request.nextUrl.pathname);
if (result) {
return NextResponse.rewrite(new URL(result, request.nextUrl));
}

if (isMarkdownPreferred(request)) {
const result = rewriteDocs(request.nextUrl.pathname);
const result = docsPathRewrite.rewrite(request.nextUrl.pathname);

if (result) {
return NextResponse.rewrite(new URL(result, request.nextUrl));
Expand Down
25 changes: 2 additions & 23 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,9 @@
},
"overrides": [
{
"includes": ["**/slides/**", "apps/web/components/landing/**"],
"includes": ["**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}", "**/*.d.ts"],
"linter": {
"rules": {
"a11y": {
"noSvgWithoutTitle": "off",
"noStaticElementInteractions": "off",
"useAriaPropsSupportedByRole": "off",
"useSemanticElements": "off"
},
"performance": {
"noImgElement": "off"
},
"complexity": {
"noUselessFragments": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"suspicious": {
"noArrayIndexKey": "off",
"noTemplateCurlyInString": "off",
"noShadowRestrictedNames": "off"
}
}
"enabled": false
}
}
],
Expand Down
13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
"typecheck": "turbo run typecheck",
"format": "biome format --write .",
"format:check": "biome format .",
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"check": "biome check .",
"check:fix": "biome check --write .",
"lint:js": "oxlint --type-aware --deny-warnings",
"lint:nonjs": "biome lint .",
"lint": "pnpm lint:js && pnpm lint:nonjs",
"lint:fix": "oxlint --type-aware --fix && biome lint --write .",
"check": "pnpm format:check && pnpm lint && pnpm typecheck",
"check:fix": "oxlint --type-aware --fix && biome check --write .",
"lint:js:no-type-aware": "oxlint --deny-warnings",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "pnpm --filter @open-slide/core test:e2e",
Expand All @@ -28,6 +31,8 @@
"@biomejs/biome": "2.4.12",
"@changesets/changelog-github": "^0.7.0",
"@changesets/cli": "^2.31.0",
"oxlint": "1.74.0",
"oxlint-tsgolint": "0.24.0",
"turbo": "^2.10.5",
"vitest": "^2.1.9"
}
Expand Down
14 changes: 8 additions & 6 deletions packages/core/src/app/components/asset-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ export function AssetView({ slideId }: Props) {
);
}

/* oxlint-disable jsx-a11y/no-noninteractive-element-interactions -- file drop zone */
return (
<section
aria-label={t.asset.sectionAria}
Expand Down Expand Up @@ -626,6 +627,7 @@ export function AssetView({ slideId }: Props) {
</section>
);
}
/* oxlint-enable jsx-a11y/no-noninteractive-element-interactions */

function EmptyState() {
const t = useLocale();
Expand Down Expand Up @@ -1130,13 +1132,13 @@ function RenameAsset({
disabled={saving}
onChange={(event) => setValue(event.target.value)}
onBlur={() => {
if (!saving) commit();
if (!saving) void commit();
}}
onKeyDown={(event) => {
if (event.nativeEvent.isComposing) return;
if (event.key === 'Enter') {
event.preventDefault();
commit();
void commit();
} else if (event.key === 'Escape') {
event.preventDefault();
onCancel();
Expand Down Expand Up @@ -1180,13 +1182,13 @@ function RenameAsset({
disabled={saving}
onChange={(e) => setValue(e.target.value)}
onBlur={() => {
if (!saving) commit();
if (!saving) void commit();
}}
onKeyDown={(e) => {
if (e.nativeEvent.isComposing) return;
if (e.key === 'Enter') {
e.preventDefault();
commit();
void commit();
} else if (e.key === 'Escape') {
e.preventDefault();
onCancel();
Expand Down Expand Up @@ -1379,7 +1381,7 @@ function LogoSearchDialog({
queueMicrotask(() => inputRef.current?.focus());
}, []);

// biome-ignore lint/correctness/useExhaustiveDependencies: retryToken is a bump-to-refetch trigger
// retryToken is a bump-to-refetch trigger; toast copy is locale-stable
useEffect(() => {
const ctrl = new AbortController();
const timer = setTimeout(() => {
Expand All @@ -1400,7 +1402,7 @@ function LogoSearchDialog({
clearTimeout(timer);
ctrl.abort();
};
}, [query, retryToken]);
}, [query, retryToken]); // oxlint-disable-line react-hooks/exhaustive-deps -- toast string is locale-stable

return (
<Dialog open onOpenChange={(open) => !open && onClose()}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function AssetPickerDialog({
[effectiveSlideId, scope, refresh, onPick, t],
);

/* oxlint-disable jsx-a11y/no-noninteractive-element-interactions -- file drop zone */
return (
<Dialog open onOpenChange={(o) => !o && onClose()}>
<DialogContent className="sm:max-w-xl">
Expand Down Expand Up @@ -93,6 +94,7 @@ export function AssetPickerDialog({
id={inputId}
type="file"
accept="image/*"
aria-label={t.asset.upload}
className="sr-only"
disabled={uploading}
onChange={(e) => {
Expand Down Expand Up @@ -142,6 +144,7 @@ export function AssetPickerDialog({
<button
key={asset.name}
type="button"
aria-label={asset.name}
onClick={() => onPick(asset, scope)}
className={cn(
'group flex flex-col overflow-hidden rounded-lg border bg-card text-left shadow-sm transition-all',
Expand Down Expand Up @@ -185,6 +188,7 @@ export function AssetPickerDialog({
</Dialog>
);
}
/* oxlint-enable jsx-a11y/no-noninteractive-element-interactions */

function hasFiles(e: React.DragEvent): boolean {
const types = e.dataTransfer?.types;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,10 @@ function ColorField({

return (
<Field label={label}>
<label className="relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-md border bg-background shadow-xs">
<label
aria-label={label}
className="relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-md border bg-background shadow-xs"
>
<span
className="size-5 rounded-sm"
style={{
Expand Down Expand Up @@ -927,7 +930,7 @@ function CommentsSection({
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
submit();
void submit();
}
}}
placeholder={t.inspector.commentPlaceholder}
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/app/components/overview-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ export function OverviewGrid({
const focusedRef = useRef<HTMLButtonElement | null>(null);
const t = useLocale();

// biome-ignore lint/correctness/useExhaustiveDependencies: only re-sync on open transition
// only re-sync focused index on open transition
useEffect(() => {
if (open) setFocused(current);
}, [open]);
}, [open]); // oxlint-disable-line react-hooks/exhaustive-deps -- intentionally ignore `current` while open

// biome-ignore lint/correctness/useExhaustiveDependencies: `focused` swaps which button holds the ref; we must re-run to focus the new node
useEffect(() => {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/app/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function Sidebar({
}
};

// biome-ignore lint/correctness/useExhaustiveDependencies: commitCreate reads latest state via stateRef
// commitCreate reads latest state via stateRef
useEffect(() => {
if (!creating) return;
const onDown = (e: MouseEvent) => {
Expand All @@ -119,11 +119,11 @@ export function Sidebar({
if (!target) return;
if (target.closest('[data-folder-create]')) return;
if (target.closest('[data-slot="popover-content"]')) return;
commitCreate();
void commitCreate();
};
document.addEventListener('mousedown', onDown);
return () => document.removeEventListener('mousedown', onDown);
}, [creating]);
}, [creating]); // oxlint-disable-line react-hooks/exhaustive-deps -- commitCreate via stateRef

return (
<aside className="relative flex h-full w-[16.5rem] shrink-0 flex-col border-r border-hairline bg-sidebar text-sidebar-foreground">
Expand Down Expand Up @@ -270,7 +270,7 @@ export function Sidebar({
onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => {
if (e.nativeEvent.isComposing) return;
if (e.key === 'Enter') commitCreate();
if (e.key === 'Enter') void commitCreate();
if (e.key === 'Escape') exitCreate();
}}
placeholder={t.home.folderName}
Expand Down
Loading