Skip to content

viteplus-ify#24

Open
t3dotgg wants to merge 5 commits intomainfrom
theo/viteplus
Open

viteplus-ify#24
t3dotgg wants to merge 5 commits intomainfrom
theo/viteplus

Conversation

@t3dotgg
Copy link
Member

@t3dotgg t3dotgg commented Mar 16, 2026

Note

Switch build tooling from Bun to vite-plus and pnpm

  • Replaces Bun with pnpm and vite-plus (vp) as the primary dev/build/lint toolchain; scripts in package.json, vercel.json, and docs/ are updated accordingly.
  • vite.config.ts now imports defineConfig from vite-plus and configures an integrated lint pipeline with oxc; tsconfigPaths plugin is removed in favor of vite-plus built-in resolution.
  • Adds VS Code settings for oxc formatter with format-on-save and a pre-commit hook running vp staged.
  • Tailwind CSS import in app/app.css is changed from a package reference to a direct path (../node_modules/tailwindcss/index.css); the external Google Fonts import is removed and the font is loaded via a <link> tag added to the root route head instead.
  • The bulk of remaining file changes are formatter-driven whitespace/style normalization with no logic changes.
  • Risk: removing baseUrl from tsconfig.json may affect TypeScript module resolution; the Tailwind path change may affect style resolution during SSR or build.

Macroscope summarized d975bb4.

Summary by CodeRabbit

  • Documentation

    • Updated setup and deployment docs, design guidelines, and CLI examples to reflect the new tooling and workflows.
  • Improvements

    • Modernized dev/build scripts and editor workspace settings.
    • Added web fonts and refined CSS formatting (no visual changes).
    • Minor copy and UI text polishing across several pages.
  • Chores

    • Added a lightweight pre-commit hook, Node version file, and Prettier ignore entry.

@vercel
Copy link

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lawn Ready Ready Preview, Comment Mar 16, 2026 11:31pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

Walkthrough

Replaces Bun tooling with pnpm/vp and integrates vite-plus (including staged/lint config), adds VS Code workspace settings and a pre-commit hook, updates CSS/font imports and Google Fonts links, removes the ESLint config, and applies widespread formatting adjustments across TS/React/Convex code and docs.

Changes

Cohort / File(s) Summary
Package manager & scripts
package.json, vercel.json, docs/deployment.md, docs/setup.md
Migrate from Bun to pnpm/vp: update packageManager, scripts, pnpm config/overrides, and docs/commands; adjust Vercel build command.
Vite-plus & build tooling
vite.config.ts, package.json
Switch config to vite-plus, add staged mapping and extensive lint config and overrides, enable tsconfigPaths in resolve, and update dependency entries to vite-plus/tsx ecosystem.
Editor & hooks
.vscode/extensions.json, .vscode/settings.json, .vite-hooks/pre-commit
Add VS Code recommendations/settings (formatter, formatOnSave, oxc options) and new pre-commit hook file containing vp staged.
CSS & fonts
app/app.css, app/routes/__root.tsx
Rework Tailwind import path and reformat many CSS blocks (no semantic changes); add Google Fonts preconnect links and Instrument Serif stylesheet to head.
ESLint config removal
eslint.config.mjs
Remove existing ESLint configuration module (default export and ignores/overrides removed).
TypeScript config
tsconfig.json
Condense arrays/objects to single-line formatting, remove baseUrl, and adjust path mappings (formatting-only/structural).
Node & tooling files
.node-version, .prettierignore, vite.config.ts (staged addition)
Add .node-version (24.14.0), add .prettierignore entry, and include staged tasks in vite config.
Docs & design content
docs/*, CLAUDE.md, plans/*, convex/README.md
Minor documentation updates: command references (bun→pnpm/vp), expanded design system content, small formatting edits across plans and README examples.
Global formatting across app & components
app/routes/*, src/components/*, src/lib/*, convex/*, scripts/*
Widespread formatting and minor refactors: collapsing multi-line imports/signatures to single-line, JSX/TSX reflow, small API/typing formatting changes, and a few small behavior adjustments (e.g., VideoPlayer event propagation, billing helpers returning value). Review emphasis: files with signature/behavior changes and larger logical edits.
Tests & route prewarm
app/routes/dashboard/-routeDataContracts.test.ts, src/lib/convexRouteData.test.ts, src/lib/useRoutePrewarmIntent.ts
Update test expectations for essential specs; tweak route prewarm debounce wiring and test mock typings. Focus review on changed expectations and debounce logic.

Sequence Diagram(s)

(silently omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'viteplus-ify' directly reflects the main objective of the PR: migrating from Bun to pnpm and adopting vite-plus as the dev toolchain, which is the primary change across the entire changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch theo/viteplus
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
app/app.css (1)

421-562: Consider migrating hardcoded color utilities to semantic Tailwind classes.

These attribute selectors mapping hardcoded hex values to CSS variables are a clever dark-mode workaround, but they're fragile—any change to the hex values in component code will silently break the mapping. This pattern also increases CSS bundle size significantly.

When time permits, consider refactoring components to use semantic Tailwind classes (e.g., bg-background, text-foreground) directly instead of hardcoded values. This would eliminate the need for these overrides.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/app.css` around lines 421 - 562, The stylesheet uses fragile attribute
selectors like [class~="bg-[`#f0f0e8`]"], [class~="text-[`#1a1a1a`]"], and
[class~="border-[`#cccccc`]"] to map hex utilities to CSS variables; replace
usages of those hardcoded utilities in components with semantic Tailwind classes
(e.g., bg-background, text-foreground, border-subtle) and remove the
corresponding attribute-selector rules (all entries mapping bg[#...],
text[#...], border[#...], hover: and group-hover: variants). If you need an
incremental path, add semantic aliases to your Tailwind config (safelist or
custom utilities) so components can switch to names like bg-surface-muted /
text-foreground-muted, update components referencing
bg-[#...]/text-[#...]/border-[#...] to the new semantic names, and once
components are migrated, delete the matching attribute-selector blocks from the
CSS.
docs/deployment.md (1)

17-19: Call out that CONVEX_DEPLOY_KEY should be Production-only.

build:vercel shells out to convex deploy, so exposing the production key to Preview would make preview builds deploy Convex too. A short note here would prevent an easy misconfiguration.

Suggested wording
 Required Vercel environment variable:
 
-- `CONVEX_DEPLOY_KEY` (create a production deploy key in Convex and add it in Vercel project settings)
+- `CONVEX_DEPLOY_KEY` (create a production deploy key in Convex and add it to the **Production** environment in Vercel project settings; do not expose it to Preview unless you intentionally want preview builds to run `convex deploy`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/deployment.md` around lines 17 - 19, Add a clear note that the
environment variable CONVEX_DEPLOY_KEY is a production-only secret because the
npm script build:vercel runs the shell command convex deploy; update
docs/deployment.md around the existing "Required Vercel environment variable"
section to explicitly state that CONVEX_DEPLOY_KEY must only be set for
Production (not Preview) in Vercel project settings to avoid preview builds
invoking convex deploy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.vite-hooks/pre-commit:
- Line 1: Add a shebang as the very first line of the pre-commit hook so the
script runs reliably across environments: insert a standard interpreter shebang
(for example using env to locate bash or sh) above the existing "vp staged"
line, and then ensure the hook file is executable (chmod +x) so Git can run it.

In `@app/app.css`:
- Around line 185-188: The CSS uses a quoted capitalized generic font family
"Georgia" in the --font-serif custom property which violates Stylelint's
value-keyword-case; update the --font-serif declaration (the --font-serif
variable) to use the lowercase generic family georgia without quotes so it reads
--font-serif: "Instrument Serif", georgia, serif; ensuring the generic keyword
is lowercase and unquoted.
- Line 5: Remove the unnecessary quotes around the single-word font name in the
font-family declarations: replace font-family: "Geist" with font-family: Geist
in all `@font-face` blocks that use "Geist" (the occurrences shown as font-family:
"Geist"); do not remove quotes for multi-word names like "Geist Mono"—leave
those quoted. Ensure all identical declarations (the repeated font-family:
"Geist") are updated consistently.

In `@app/routes/__root.tsx`:
- Around line 30-35: The preconnect for "https://fonts.googleapis.com" is
missing crossOrigin like the "https://fonts.gstatic.com" entry; update the
preconnect link object for fonts.googleapis.com to include crossOrigin:
"anonymous" for consistency, and (unless block behavior is intentional) change
the stylesheet href query from display=block to display=swap to avoid
FOIT—locate the link objects with rel: "preconnect" and the stylesheet href
containing "fonts.googleapis.com/css2?family=Instrument+Serif" in the array and
apply these two changes.

---

Nitpick comments:
In `@app/app.css`:
- Around line 421-562: The stylesheet uses fragile attribute selectors like
[class~="bg-[`#f0f0e8`]"], [class~="text-[`#1a1a1a`]"], and
[class~="border-[`#cccccc`]"] to map hex utilities to CSS variables; replace
usages of those hardcoded utilities in components with semantic Tailwind classes
(e.g., bg-background, text-foreground, border-subtle) and remove the
corresponding attribute-selector rules (all entries mapping bg[#...],
text[#...], border[#...], hover: and group-hover: variants). If you need an
incremental path, add semantic aliases to your Tailwind config (safelist or
custom utilities) so components can switch to names like bg-surface-muted /
text-foreground-muted, update components referencing
bg-[#...]/text-[#...]/border-[#...] to the new semantic names, and once
components are migrated, delete the matching attribute-selector blocks from the
CSS.

In `@docs/deployment.md`:
- Around line 17-19: Add a clear note that the environment variable
CONVEX_DEPLOY_KEY is a production-only secret because the npm script
build:vercel runs the shell command convex deploy; update docs/deployment.md
around the existing "Required Vercel environment variable" section to explicitly
state that CONVEX_DEPLOY_KEY must only be set for Production (not Preview) in
Vercel project settings to avoid preview builds invoking convex deploy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 48d6cd77-7336-4ade-8f59-781d4c4cdc07

📥 Commits

Reviewing files that changed from the base of the PR and between 046c2b2 and 17e434a.

⛔ Files ignored due to path filters (3)
  • bun.lock is excluded by !**/*.lock
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • .vite-hooks/pre-commit
  • .vscode/extensions.json
  • .vscode/settings.json
  • app/app.css
  • app/routes/__root.tsx
  • docs/deployment.md
  • docs/setup.md
  • eslint.config.mjs
  • package.json
  • tsconfig.json
  • vercel.json
  • vite.config.ts
💤 Files with no reviewable changes (1)
  • eslint.config.mjs

@@ -0,0 +1 @@
vp staged
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a shebang to ensure proper script execution.

Pre-commit hooks need a shebang to be reliably executed across different environments. Without it, the hook may fail depending on how Git invokes it.

🔧 Proposed fix
+#!/bin/sh
 vp staged
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
vp staged
#!/bin/sh
vp staged
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vite-hooks/pre-commit at line 1, Add a shebang as the very first line of
the pre-commit hook so the script runs reliably across environments: insert a
standard interpreter shebang (for example using env to locate bash or sh) above
the existing "vp staged" line, and then ensure the hook file is executable
(chmod +x) so Git can run it.

@font-face {
font-family: 'Geist';
src: url('https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-sans/Geist-Regular.woff2') format('woff2');
font-family: "Geist";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unnecessary quotes around font-family name.

Stylelint flags that "Geist" doesn't need quotes since it's a single-word font name without spaces or special characters. This applies to all @font-face declarations (lines 5, 14, 23, 32, 41, 50, 59).

🔧 Proposed fix (example for first occurrence)
 `@font-face` {
-  font-family: "Geist";
+  font-family: Geist;
   src: url("https://cdn.jsdelivr.net/npm/geist@1.2.0/dist/fonts/geist-sans/Geist-Regular.woff2")

Apply similar changes to:

  • Line 14, 23, 32, 41: "Geist"Geist
  • Line 50, 59: "Geist Mono" (keep quotes - has space)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
font-family: "Geist";
font-family: Geist;
🧰 Tools
🪛 Stylelint (17.4.0)

[error] 5-5: Unexpected quotes around "Geist" (font-family-name-quotes)

(font-family-name-quotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/app.css` at line 5, Remove the unnecessary quotes around the single-word
font name in the font-family declarations: replace font-family: "Geist" with
font-family: Geist in all `@font-face` blocks that use "Geist" (the occurrences
shown as font-family: "Geist"); do not remove quotes for multi-word names like
"Geist Mono"—leave those quoted. Ensure all identical declarations (the repeated
font-family: "Geist") are updated consistently.

Comment on lines +185 to 188
--font-sans: "Geist", system-ui, -apple-system, sans-serif;
--font-mono: "Geist Mono", ui-monospace, monospace;
--font-serif: "Instrument Serif", Georgia, serif;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix casing for generic font family.

Per Stylelint's value-keyword-case rule, generic font family keywords should be lowercase. Change "Georgia" to georgia (unquoted) on line 187.

🔧 Proposed fix
   --font-sans: "Geist", system-ui, -apple-system, sans-serif;
   --font-mono: "Geist Mono", ui-monospace, monospace;
-  --font-serif: "Instrument Serif", Georgia, serif;
+  --font-serif: "Instrument Serif", georgia, serif;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
--font-sans: "Geist", system-ui, -apple-system, sans-serif;
--font-mono: "Geist Mono", ui-monospace, monospace;
--font-serif: "Instrument Serif", Georgia, serif;
}
--font-sans: "Geist", system-ui, -apple-system, sans-serif;
--font-mono: "Geist Mono", ui-monospace, monospace;
--font-serif: "Instrument Serif", georgia, serif;
}
🧰 Tools
🪛 Biome (2.4.7)

[error] 182-188: Tailwind-specific syntax is disabled.

(parse)

🪛 Stylelint (17.4.0)

[error] 187-187: Expected "Georgia" to be "georgia" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/app.css` around lines 185 - 188, The CSS uses a quoted capitalized
generic font family "Georgia" in the --font-serif custom property which violates
Stylelint's value-keyword-case; update the --font-serif declaration (the
--font-serif variable) to use the lowercase generic family georgia without
quotes so it reads --font-serif: "Instrument Serif", georgia, serif; ensuring
the generic keyword is lowercase and unquoted.

Comment on lines +30 to +35
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=block",
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add crossOrigin to the googleapis.com preconnect for consistency.

The fonts.gstatic.com preconnect has crossOrigin: "anonymous" but fonts.googleapis.com does not. For proper CORS credentialing and connection reuse, both preconnects should have it.

Also consider using display=swap instead of display=block to avoid Flash of Invisible Text (FOIT) while fonts load—unless the block behavior is intentional for design consistency.

🔧 Proposed fix for crossOrigin
-      { rel: "preconnect", href: "https://fonts.googleapis.com" },
+      { rel: "preconnect", href: "https://fonts.googleapis.com", crossOrigin: "anonymous" },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=block",
},
{ rel: "preconnect", href: "https://fonts.googleapis.com", crossOrigin: "anonymous" },
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" },
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=block",
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/__root.tsx` around lines 30 - 35, The preconnect for
"https://fonts.googleapis.com" is missing crossOrigin like the
"https://fonts.gstatic.com" entry; update the preconnect link object for
fonts.googleapis.com to include crossOrigin: "anonymous" for consistency, and
(unless block behavior is intentional) change the stylesheet href query from
display=block to display=swap to avoid FOIT—locate the link objects with rel:
"preconnect" and the stylesheet href containing
"fonts.googleapis.com/css2?family=Instrument+Serif" in the array and apply these
two changes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/setup.md`:
- Around line 5-6: Update the docs to clarify the prerequisite step by adding an
explicit instruction to run pnpm install before referencing the vp CLI;
specifically mention running "pnpm install" so that vite+ (and the vp command)
is installed and available prior to the subsequent "vp install" instruction, and
update the sentence referencing "Install vite+ first" to include this pnpm
install step for clarity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e16af569-bb73-43e7-86da-ce053be5b727

📥 Commits

Reviewing files that changed from the base of the PR and between 17e434a and bc4929f.

📒 Files selected for processing (1)
  • docs/setup.md

Comment on lines +5 to +6
Install vite+ first. Then...

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether prerequisite installation steps for `vp` are documented elsewhere.
rg -n -C2 'vite\+|vite-plus|vp install|install.*vp' docs README.md

Repository: pingdotgg/lawn

Length of output: 315


🏁 Script executed:

cat -n docs/setup.md

Repository: pingdotgg/lawn

Length of output: 1244


🏁 Script executed:

rg -n 'vp' docs/setup.md | head -20

Repository: pingdotgg/lawn

Length of output: 170


🌐 Web query:

vite+ vp package manager install

💡 Result:

Vite (app scaffold) — install via your package manager

Use the Vite “create” CLI (it installs/runs without a separate global install):

  • npm
    npm create vite@latest
  • pnpm
    pnpm create vite@latest
  • yarn
    yarn create vite
  • bun
    bun create vite
    (vite.dev)

VitePress (docs site) — install + init wizard

Install VitePress, then run the init wizard:

  • Install
    npm add -D vitepress@next
    # or: pnpm add -D vitepress@next
    # or: yarn add -D vitepress@next vue
    # or: bun add -D vitepress@next
  • Initialize
    npx vitepress init
    # or: pnpm vitepress init
    # or: yarn vitepress init
    # or: bun vitepress init
    (vitepress.dev)

Citations:


🏁 Script executed:

fd -type f -name "package.json" -o -name "README*" | head -10

Repository: pingdotgg/lawn

Length of output: 228


🏁 Script executed:

rg -n 'vp.*install|npm install.*vp|install vite\+' --type-list

Repository: pingdotgg/lawn

Length of output: 447


🏁 Script executed:

# Search for vp bin or setup references
rg -n 'bin.*vp|vp.*bin|install.*command' | head -20

Repository: pingdotgg/lawn

Length of output: 40


🏁 Script executed:

# Check if package.json has vp as a script
cat package.json 2>/dev/null | head -50

Repository: pingdotgg/lawn

Length of output: 1882


🏁 Script executed:

# Check devDependencies and other dep sections in package.json
tail -100 package.json | grep -A 50 "devDependencies"

Repository: pingdotgg/lawn

Length of output: 1217


🏁 Script executed:

# Search for "vp" in package.json to see if it's listed anywhere
rg -n '"vp"' package.json

Repository: pingdotgg/lawn

Length of output: 40


🏁 Script executed:

# Check README for installation or setup instructions
cat README.md 2>/dev/null | head -100

Repository: pingdotgg/lawn

Length of output: 871


🏁 Script executed:

# Search for any docs mentioning vp or vite+
fd -name "*.md" | xargs rg -l "vp|vite\+" | head -10

Repository: pingdotgg/lawn

Length of output: 374


Clarify the initial setup step before using vp.

"Install vite+ first" is missing the crucial prerequisite. The documentation should explicitly instruct users to run pnpm install first, which installs vite-plus and makes the vp CLI available. Without this, the next line's vp install command will fail.

Add an explicit instruction:

📝 Suggested doc patch
 Install vite+ first. Then...

+```bash
+pnpm install
+```
+
 Install dependencies:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/setup.md` around lines 5 - 6, Update the docs to clarify the
prerequisite step by adding an explicit instruction to run pnpm install before
referencing the vp CLI; specifically mention running "pnpm install" so that
vite+ (and the vp command) is installed and available prior to the subsequent
"vp install" instruction, and update the sentence referencing "Install vite+
first" to include this pnpm install step for clarity.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (4)
app/routes/-watch.tsx (2)

288-293: Consider deduplicating the submit button/form chunk.

Both desktop and mobile composer sections use near-identical submit button props/content; extracting shared JSX would simplify future updates.

Also applies to: 406-411

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/-watch.tsx` around lines 288 - 293, Extract the repeated submit
button JSX into a small shared component (e.g., CommentSubmitButton or
ComposerSubmitButton) that accepts props used by both instances: commentText,
isSubmittingComment, onSubmit (or type/size/className if needed), and any
children/label; replace the two near-identical inline Button usages with this
new component and forward the disabled logic (disabled={!commentText.trim() ||
isSubmittingComment}) and other props (type="submit", size="sm",
className="w-full") to keep behavior identical; update the form/compose areas to
import/use the new component so future changes require editing only one place.

240-245: Consider extracting shared comment-content rendering.

The desktop and mobile blocks now repeat the same comment text/time rendering. A small shared component/helper would reduce drift risk.

Also applies to: 357-362

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/-watch.tsx` around lines 240 - 245, Extract the duplicated markup
into a small React component (e.g., CommentContent or CommentText) that accepts
the comment (or props text and _creationTime) and returns the two <p> elements
using comment.text and formatRelativeTime(comment._creationTime) with the same
classNames; then replace the repeated blocks in both desktop and mobile render
paths with a single <CommentContent comment={comment} /> (or <CommentText
text={comment.text} time={comment._creationTime} />) to remove duplication and
keep rendering consistent.
src/components/ui/card.tsx (1)

25-33: Pre-existing type mismatch in CardTitle.

The ref type is HTMLParagraphElement but the component renders an <h3> element, and the props extend React.HTMLAttributes<HTMLHeadingElement>. This inconsistency predates this PR but may cause type errors when passing a ref. Consider aligning types:

🔧 Suggested fix
-const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
+const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/card.tsx` around lines 25 - 33, CardTitle's forwardRef type
parameters are inconsistent: the ref is declared as HTMLParagraphElement while
the component renders an <h3> and uses React.HTMLAttributes<HTMLHeadingElement>,
which can cause type errors when consumers pass a ref. Fix by aligning the
generic types for React.forwardRef so the ref type and props match the rendered
element (e.g., use HTMLHeadingElement for the ref and keep
React.HTMLAttributes<HTMLHeadingElement> for props, or switch to
React.ComponentPropsWithRef<'h3'>); update the CardTitle forwardRef signature to
use the chosen matching types so ref and props are consistent with the <h3>
element.
src/components/comments/CommentInput.tsx (1)

107-112: Remove redundant ternary in action-row className.

Both branches return the same string, so the conditional can be simplified.

Suggested cleanup
-      <div
-        className={
-          variant === "seamless"
-            ? "absolute bottom-3 right-3 flex items-center gap-2"
-            : "absolute bottom-3 right-3 flex items-center gap-2"
-        }
-      >
+      <div className="absolute bottom-3 right-3 flex items-center gap-2">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/comments/CommentInput.tsx` around lines 107 - 112, In
CommentInput.tsx remove the redundant ternary used for the action-row div
className: replace the conditional expression that checks variant === "seamless"
and returns the same string in both branches with a single constant string
("absolute bottom-3 right-3 flex items-center gap-2"); update the JSX in the
CommentInput component where the div with className currently uses the ternary
so it directly uses the static className value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/dashboard/-project.tsx`:
- Around line 336-337: The Download action is shown for videos in "processing"
even though convex/videoActions.ts:getDownloadUrl only allows downloads when
video.status === "ready"; update the canDownload predicate used in both the grid
and list branches to require video.status === "ready" (e.g. canDownload =
Boolean(video.s3Key) && video.status === "ready") so the UI hides Download until
the video is actually ready.
- Around line 381-389: The menu trigger button is hidden via "opacity-0
group-hover:opacity-100" which prevents touch/keyboard users from
seeing/focusing it and the icon-only button lacks an accessible name; update the
DropdownMenu trigger usage (the DropdownMenuTrigger and its child button using
the MoreVertical icon) to be visible by removing the opacity-0/group-hover
classes (or make it always have sufficient opacity/visibility) and add an
accessible label to the icon-only button (e.g., aria-label="Open actions" or
include a screen-reader-only text) so screen readers and keyboard users can
identify it; apply the same visibility and labeling changes to the list-view
trigger instance referenced later (the other DropdownMenuTrigger/button block).

In `@app/routes/dashboard/-video.tsx`:
- Around line 267-303: The inline title editor lacks accessible names: add
descriptive accessible labels to the Input and the three icon Buttons so screen
readers announce their purpose; specifically, give the Input (used with
editedTitle) an aria-label like "Edit video title" or aria-labelledby pointing
to a visible label, and add aria-labels to the save Button (handleSaveTitle)
e.g. "Save title", the cancel Button (setIsEditingTitle(false)) e.g. "Cancel
rename", and the edit Button (startEditingTitle) e.g. "Rename video"; ensure
these attributes are applied to the Input and Button components used in the
isEditingTitle conditional so assistive tech can identify each control.

In `@convex/billing.ts`:
- Around line 193-197: subscriptionStatus can come from subscription?.status or
fallback to subscriptionState.team.billingStatus, but hasActiveSubscription is
currently derived only from subscription and can contradict subscriptionStatus;
update the logic so hasActiveSubscription uses the resolved subscriptionStatus
(or the same fallback order) instead of subscription-only data. Specifically,
compute subscriptionStatus using subscription?.status ??
subscriptionState.team.billingStatus ?? null and then set hasActiveSubscription
by evaluating that resolved status (e.g., === "active") or the same fallback
expression, ensuring consistency between subscriptionStatus and
hasActiveSubscription.

In `@src/components/comments/CommentInput.tsx`:
- Around line 77-80: The Enter-key handler that calls submitComment currently
doesn't prevent submission during IME composition; update the keydown handler
(the block checking e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey
&& !e.altKey) to also check for IME composition by verifying
!e.nativeEvent.isComposing before calling e.preventDefault() and
submitComment(), and for broader compatibility add a fallback that treats
nativeEvent.keyCode === 229 as composing (or alternatively implement
onCompositionStart/onCompositionEnd to track a composing flag and check that
flag here); ensure you reference and update the same handler around the
submitComment invocation so composition prevents accidental submits.

In `@src/components/video-player/VideoPlayer.tsx`:
- Around line 829-832: In VideoPlayer.tsx the volume slider only calls
e.stopPropagation() inside the onChange handler (where setVideoVolume is
called), allowing parent pointer/click handlers to still fire; add onPointerDown
and onClick handlers on the same slider element that call e.stopPropagation()
(and prevent default if needed) to fully isolate interactions with the control
so parent listeners won't receive pointer or click events when the user
manipulates the slider.

---

Nitpick comments:
In `@app/routes/-watch.tsx`:
- Around line 288-293: Extract the repeated submit button JSX into a small
shared component (e.g., CommentSubmitButton or ComposerSubmitButton) that
accepts props used by both instances: commentText, isSubmittingComment, onSubmit
(or type/size/className if needed), and any children/label; replace the two
near-identical inline Button usages with this new component and forward the
disabled logic (disabled={!commentText.trim() || isSubmittingComment}) and other
props (type="submit", size="sm", className="w-full") to keep behavior identical;
update the form/compose areas to import/use the new component so future changes
require editing only one place.
- Around line 240-245: Extract the duplicated markup into a small React
component (e.g., CommentContent or CommentText) that accepts the comment (or
props text and _creationTime) and returns the two <p> elements using
comment.text and formatRelativeTime(comment._creationTime) with the same
classNames; then replace the repeated blocks in both desktop and mobile render
paths with a single <CommentContent comment={comment} /> (or <CommentText
text={comment.text} time={comment._creationTime} />) to remove duplication and
keep rendering consistent.

In `@src/components/comments/CommentInput.tsx`:
- Around line 107-112: In CommentInput.tsx remove the redundant ternary used for
the action-row div className: replace the conditional expression that checks
variant === "seamless" and returns the same string in both branches with a
single constant string ("absolute bottom-3 right-3 flex items-center gap-2");
update the JSX in the CommentInput component where the div with className
currently uses the ternary so it directly uses the static className value.

In `@src/components/ui/card.tsx`:
- Around line 25-33: CardTitle's forwardRef type parameters are inconsistent:
the ref is declared as HTMLParagraphElement while the component renders an <h3>
and uses React.HTMLAttributes<HTMLHeadingElement>, which can cause type errors
when consumers pass a ref. Fix by aligning the generic types for
React.forwardRef so the ref type and props match the rendered element (e.g., use
HTMLHeadingElement for the ref and keep React.HTMLAttributes<HTMLHeadingElement>
for props, or switch to React.ComponentPropsWithRef<'h3'>); update the CardTitle
forwardRef signature to use the chosen matching types so ref and props are
consistent with the <h3> element.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ed95175-281d-4b98-b396-5f812150c53a

📥 Commits

Reviewing files that changed from the base of the PR and between bc4929f and d975bb4.

⛔ Files ignored due to path filters (1)
  • convex/_generated/dataModel.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (94)
  • .node-version
  • .prettierignore
  • CLAUDE.md
  • app/routes/-compare-frameio.tsx
  • app/routes/-compare-wipster.tsx
  • app/routes/-for-agencies.tsx
  • app/routes/-for-video-editors.tsx
  • app/routes/-home.tsx
  • app/routes/-invite.data.ts
  • app/routes/-invite.tsx
  • app/routes/-pricing.tsx
  • app/routes/-share.data.ts
  • app/routes/-share.tsx
  • app/routes/-watch.data.ts
  • app/routes/-watch.tsx
  • app/routes/auth/-layout.tsx
  • app/routes/dashboard/$teamSlug.$projectId.index.tsx
  • app/routes/dashboard/-index.data.ts
  • app/routes/dashboard/-layout.tsx
  • app/routes/dashboard/-project.data.ts
  • app/routes/dashboard/-project.tsx
  • app/routes/dashboard/-routeDataContracts.test.ts
  • app/routes/dashboard/-settings.data.ts
  • app/routes/dashboard/-settings.tsx
  • app/routes/dashboard/-team.data.ts
  • app/routes/dashboard/-team.tsx
  • app/routes/dashboard/-useVideoUploadManager.ts
  • app/routes/dashboard/-video.data.ts
  • app/routes/dashboard/-video.tsx
  • app/routes/dashboard/index.tsx
  • app/routes/mono.tsx
  • app/routes/sign-in.tsx
  • app/routes/sign-up.tsx
  • convex/README.md
  • convex/auth.ts
  • convex/billing.ts
  • convex/billingHelpers.ts
  • convex/comments.ts
  • convex/mux.ts
  • convex/muxActions.ts
  • convex/projects.ts
  • convex/s3.ts
  • convex/schema.ts
  • convex/security.ts
  • convex/shareAccess.ts
  • convex/shareLinks.ts
  • convex/teams.ts
  • convex/videoActions.ts
  • convex/videoPresence.ts
  • convex/videos.ts
  • convex/workspace.ts
  • docs/philosophy.md
  • plans/fix-navigation.md
  • plans/remove-user-table.md
  • scripts/generate-og.tsx
  • src/components/DashboardHeader.tsx
  • src/components/MarketingFooter.tsx
  • src/components/MarketingNav.tsx
  • src/components/ShareDialog.tsx
  • src/components/comments/CommentInput.tsx
  • src/components/comments/CommentItem.tsx
  • src/components/comments/CommentList.tsx
  • src/components/presence/VideoWatchers.tsx
  • src/components/teams/CreateTeamDialog.tsx
  • src/components/teams/MemberInvite.tsx
  • src/components/theme/ThemeToggle.tsx
  • src/components/ui/NotFound.tsx
  • src/components/ui/avatar.tsx
  • src/components/ui/badge.tsx
  • src/components/ui/button.tsx
  • src/components/ui/card.tsx
  • src/components/ui/dialog.tsx
  • src/components/ui/dropdown-menu.tsx
  • src/components/ui/input.tsx
  • src/components/ui/progress.tsx
  • src/components/ui/scroll-area.tsx
  • src/components/ui/separator.tsx
  • src/components/ui/tabs.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/tooltip.tsx
  • src/components/upload/DropZone.tsx
  • src/components/upload/UploadButton.tsx
  • src/components/upload/UploadProgress.tsx
  • src/components/video-player/VideoPlayer.tsx
  • src/components/videos/VideoWorkflowStatusControl.tsx
  • src/lib/convexRouteData.test.ts
  • src/lib/convexRouteData.ts
  • src/lib/dashboardUploadContext.tsx
  • src/lib/download.ts
  • src/lib/seo.ts
  • src/lib/useRoutePrewarmIntent.ts
  • src/lib/useVideoPresence.ts
  • vercel.json
  • vite.config.ts
💤 Files with no reviewable changes (3)
  • src/lib/download.ts
  • plans/fix-navigation.md
  • plans/remove-user-table.md
✅ Files skipped from review due to trivial changes (52)
  • src/components/ui/progress.tsx
  • app/routes/dashboard/-useVideoUploadManager.ts
  • app/routes/-compare-wipster.tsx
  • app/routes/dashboard/-index.data.ts
  • src/components/ui/tabs.tsx
  • src/lib/convexRouteData.ts
  • src/components/ui/badge.tsx
  • src/lib/useVideoPresence.ts
  • src/lib/seo.ts
  • src/components/teams/CreateTeamDialog.tsx
  • convex/README.md
  • src/lib/dashboardUploadContext.tsx
  • src/components/theme/ThemeToggle.tsx
  • convex/shareLinks.ts
  • app/routes/sign-in.tsx
  • convex/projects.ts
  • .prettierignore
  • app/routes/-invite.data.ts
  • app/routes/-pricing.tsx
  • app/routes/-share.tsx
  • app/routes/-compare-frameio.tsx
  • src/components/presence/VideoWatchers.tsx
  • convex/muxActions.ts
  • src/components/comments/CommentItem.tsx
  • src/components/MarketingNav.tsx
  • src/components/ui/tooltip.tsx
  • convex/s3.ts
  • src/components/DashboardHeader.tsx
  • app/routes/dashboard/$teamSlug.$projectId.index.tsx
  • app/routes/dashboard/index.tsx
  • src/components/upload/UploadButton.tsx
  • app/routes/dashboard/-project.data.ts
  • src/components/teams/MemberInvite.tsx
  • src/components/ShareDialog.tsx
  • src/components/ui/input.tsx
  • .node-version
  • src/components/ui/avatar.tsx
  • src/components/MarketingFooter.tsx
  • app/routes/dashboard/-video.data.ts
  • app/routes/dashboard/-team.data.ts
  • app/routes/dashboard/-settings.tsx
  • scripts/generate-og.tsx
  • convex/workspace.ts
  • src/components/ui/NotFound.tsx
  • app/routes/-for-agencies.tsx
  • app/routes/-home.tsx
  • src/components/videos/VideoWorkflowStatusControl.tsx
  • convex/auth.ts
  • convex/teams.ts
  • src/components/ui/button.tsx
  • src/components/upload/DropZone.tsx
  • app/routes/-invite.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • vercel.json

Comment on lines +336 to +337
const canDownload =
Boolean(video.s3Key) && video.status !== "failed" && video.status !== "uploading";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the Download action hidden until the video is ready.

Lines 336-337 and Lines 475-476 still treat processing videos as downloadable, but convex/videoActions.ts Lines 480-482 reject getDownloadUrl unless video.status === "ready". Right now the menu can surface a Download action that always fails for videos still encoding.

💡 Suggested fix
-                const canDownload =
-                  Boolean(video.s3Key) && video.status !== "failed" && video.status !== "uploading";
+                const canDownload = Boolean(video.s3Key) && video.status === "ready";

Apply the same predicate in both the grid and list branches.

Also applies to: 475-476

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/dashboard/-project.tsx` around lines 336 - 337, The Download
action is shown for videos in "processing" even though
convex/videoActions.ts:getDownloadUrl only allows downloads when video.status
=== "ready"; update the canDownload predicate used in both the grid and list
branches to require video.status === "ready" (e.g. canDownload =
Boolean(video.s3Key) && video.status === "ready") so the UI hides Download until
the video is actually ready.

Comment on lines +381 to +389
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<DropdownMenu>
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
<button
type="button"
className="inline-flex h-8 w-8 cursor-pointer items-center justify-center bg-black/60 hover:bg-black/80 text-white"
>
<Download className="mr-2 h-4 w-4" />
Download
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
void handleShareVideo(video);
}}
>
<LinkIcon className="mr-2 h-4 w-4" />
Share
</DropdownMenuItem>
{canUpload && (
<MoreVertical className="h-4 w-4" />
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Expose the action menu trigger without hover.

Lines 381-389 and Lines 556-564 hide the only menu trigger behind group-hover, so touch users never see it and keyboard users can tab onto an invisible button. The trigger also needs an accessible name because it's icon-only.

💡 Suggested fix
-                      <div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
+                      <div className="absolute top-2 right-2 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100 transition-opacity">
...
-                            <button
-                              type="button"
-                              className="inline-flex h-8 w-8 cursor-pointer items-center justify-center bg-black/60 hover:bg-black/80 text-white"
-                            >
+                            <button
+                              type="button"
+                              aria-label={`Open actions for ${video.title}`}
+                              className="inline-flex h-8 w-8 cursor-pointer items-center justify-center bg-black/60 hover:bg-black/80 text-white"
+                            >

Apply the same visibility and labeling change to the list-view trigger as well.

Also applies to: 556-564

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/dashboard/-project.tsx` around lines 381 - 389, The menu trigger
button is hidden via "opacity-0 group-hover:opacity-100" which prevents
touch/keyboard users from seeing/focusing it and the icon-only button lacks an
accessible name; update the DropdownMenu trigger usage (the DropdownMenuTrigger
and its child button using the MoreVertical icon) to be visible by removing the
opacity-0/group-hover classes (or make it always have sufficient
opacity/visibility) and add an accessible label to the icon-only button (e.g.,
aria-label="Open actions" or include a screen-reader-only text) so screen
readers and keyboard users can identify it; apply the same visibility and
labeling changes to the list-view trigger instance referenced later (the other
DropdownMenuTrigger/button block).

Comment on lines +267 to +303
label: isEditingTitle ? (
<div className="flex items-center gap-2">
<Input
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
className="w-40 sm:w-64 h-8 text-base font-black tracking-tighter uppercase font-mono"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveTitle();
if (e.key === "Escape") setIsEditingTitle(false);
}}
/>
<Button size="icon" variant="ghost" className="h-8 w-8" onClick={handleSaveTitle}>
<Check className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={startEditingTitle}
className="h-8 w-8"
onClick={() => setIsEditingTitle(false)}
>
<Edit2 className="h-3 w-3" />
<X className="h-4 w-4" />
</Button>
)}
{video.status !== "ready" && (
<Badge
variant={video.status === "failed" ? "destructive" : "secondary"}
>
{video.status === "uploading" && "Uploading"}
{video.status === "processing" && "Processing"}
{video.status === "failed" && "Failed"}
</Badge>
)}
</div>
)
}
]}>
</div>
) : (
<div className="flex items-center gap-2">
<span className="truncate max-w-[150px] sm:max-w-[300px]">{video.title}</span>
{canEdit && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={startEditingTitle}
>
<Edit2 className="h-3 w-3" />
</Button>
)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add accessible names to the inline title editor controls.

Lines 267-303 introduce a new rename flow, but the input and the save/cancel/edit icon buttons are all unnamed. Screen readers will announce anonymous controls here, which makes the title-edit path much harder to use.

💡 Suggested fix
                 <Input
+                  aria-label="Video title"
                   value={editedTitle}
                   onChange={(e) => setEditedTitle(e.target.value)}
                   className="w-40 sm:w-64 h-8 text-base font-black tracking-tighter uppercase font-mono"
                   autoFocus
                   onKeyDown={(e) => {
                     if (e.key === "Enter") handleSaveTitle();
                     if (e.key === "Escape") setIsEditingTitle(false);
                   }}
                 />
-                <Button size="icon" variant="ghost" className="h-8 w-8" onClick={handleSaveTitle}>
+                <Button
+                  type="button"
+                  size="icon"
+                  variant="ghost"
+                  className="h-8 w-8"
+                  aria-label="Save title"
+                  onClick={handleSaveTitle}
+                >
                   <Check className="h-4 w-4" />
                 </Button>
                 <Button
+                  type="button"
                   size="icon"
                   variant="ghost"
                   className="h-8 w-8"
+                  aria-label="Cancel title editing"
                   onClick={() => setIsEditingTitle(false)}
                 >
                   <X className="h-4 w-4" />
                 </Button>
...
                 {canEdit && (
                   <Button
+                    type="button"
                     size="icon"
                     variant="ghost"
                     className="h-6 w-6"
+                    aria-label="Edit video title"
                     onClick={startEditingTitle}
                   >
                     <Edit2 className="h-3 w-3" />
                   </Button>
                 )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
label: isEditingTitle ? (
<div className="flex items-center gap-2">
<Input
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
className="w-40 sm:w-64 h-8 text-base font-black tracking-tighter uppercase font-mono"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveTitle();
if (e.key === "Escape") setIsEditingTitle(false);
}}
/>
<Button size="icon" variant="ghost" className="h-8 w-8" onClick={handleSaveTitle}>
<Check className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={startEditingTitle}
className="h-8 w-8"
onClick={() => setIsEditingTitle(false)}
>
<Edit2 className="h-3 w-3" />
<X className="h-4 w-4" />
</Button>
)}
{video.status !== "ready" && (
<Badge
variant={video.status === "failed" ? "destructive" : "secondary"}
>
{video.status === "uploading" && "Uploading"}
{video.status === "processing" && "Processing"}
{video.status === "failed" && "Failed"}
</Badge>
)}
</div>
)
}
]}>
</div>
) : (
<div className="flex items-center gap-2">
<span className="truncate max-w-[150px] sm:max-w-[300px]">{video.title}</span>
{canEdit && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6"
onClick={startEditingTitle}
>
<Edit2 className="h-3 w-3" />
</Button>
)}
label: isEditingTitle ? (
<div className="flex items-center gap-2">
<Input
aria-label="Video title"
value={editedTitle}
onChange={(e) => setEditedTitle(e.target.value)}
className="w-40 sm:w-64 h-8 text-base font-black tracking-tighter uppercase font-mono"
autoFocus
onKeyDown={(e) => {
if (e.key === "Enter") handleSaveTitle();
if (e.key === "Escape") setIsEditingTitle(false);
}}
/>
<Button
type="button"
size="icon"
variant="ghost"
className="h-8 w-8"
aria-label="Save title"
onClick={handleSaveTitle}
>
<Check className="h-4 w-4" />
</Button>
<Button
type="button"
size="icon"
variant="ghost"
className="h-8 w-8"
aria-label="Cancel title editing"
onClick={() => setIsEditingTitle(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex items-center gap-2">
<span className="truncate max-w-[150px] sm:max-w-[300px]">{video.title}</span>
{canEdit && (
<Button
type="button"
size="icon"
variant="ghost"
className="h-6 w-6"
aria-label="Edit video title"
onClick={startEditingTitle}
>
<Edit2 className="h-3 w-3" />
</Button>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/dashboard/-video.tsx` around lines 267 - 303, The inline title
editor lacks accessible names: add descriptive accessible labels to the Input
and the three icon Buttons so screen readers announce their purpose;
specifically, give the Input (used with editedTitle) an aria-label like "Edit
video title" or aria-labelledby pointing to a visible label, and add aria-labels
to the save Button (handleSaveTitle) e.g. "Save title", the cancel Button
(setIsEditingTitle(false)) e.g. "Cancel rename", and the edit Button
(startEditingTitle) e.g. "Rename video"; ensure these attributes are applied to
the Input and Button components used in the isEditingTitle conditional so
assistive tech can identify each control.

Comment on lines +193 to +197
subscriptionStatus: subscription?.status ?? subscriptionState.team.billingStatus ?? null,
stripeCustomerId:
subscriptionState.team.stripeCustomerId ??
subscription?.stripeCustomerId ??
null,
subscriptionState.team.stripeCustomerId ?? subscription?.stripeCustomerId ?? null,
stripeSubscriptionId:
subscription?.stripeSubscriptionId ??
subscriptionState.team.stripeSubscriptionId ??
null,
subscription?.stripeSubscriptionId ?? subscriptionState.team.stripeSubscriptionId ?? null,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep hasActiveSubscription consistent with fallback status.

With the new fallback on Line 193 and Lines 195–197, subscriptionStatus can come from team state while hasActiveSubscription still comes from subscription-only state. This can return contradictory values (e.g., status "active" but hasActiveSubscription: false).

Suggested fix
   handler: async (ctx, args) => {
     const { membership } = await requireTeamAccess(ctx, args.teamId);
     const subscriptionState = await getTeamSubscriptionState(ctx, args.teamId);
     const storageUsedBytes = await getTeamStorageUsedBytes(ctx, args.teamId);
     const subscription = subscriptionState.subscription;
+    const effectiveStatus = subscription?.status ?? subscriptionState.team.billingStatus ?? null;
+    const hasActiveSubscription = hasActiveTeamSubscriptionStatus(effectiveStatus);

     return {
       plan: subscriptionState.plan,
       monthlyPriceUsd: TEAM_PLAN_MONTHLY_PRICE_USD[subscriptionState.plan],
       storageLimitBytes: TEAM_PLAN_STORAGE_LIMIT_BYTES[subscriptionState.plan],
       storageUsedBytes,
-      hasActiveSubscription: subscriptionState.hasActiveSubscription,
-      subscriptionStatus: subscription?.status ?? subscriptionState.team.billingStatus ?? null,
+      hasActiveSubscription,
+      subscriptionStatus: effectiveStatus,
       stripeCustomerId:
         subscriptionState.team.stripeCustomerId ?? subscription?.stripeCustomerId ?? null,
       stripeSubscriptionId:
         subscription?.stripeSubscriptionId ?? subscriptionState.team.stripeSubscriptionId ?? null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@convex/billing.ts` around lines 193 - 197, subscriptionStatus can come from
subscription?.status or fallback to subscriptionState.team.billingStatus, but
hasActiveSubscription is currently derived only from subscription and can
contradict subscriptionStatus; update the logic so hasActiveSubscription uses
the resolved subscriptionStatus (or the same fallback order) instead of
subscription-only data. Specifically, compute subscriptionStatus using
subscription?.status ?? subscriptionState.team.billingStatus ?? null and then
set hasActiveSubscription by evaluating that resolved status (e.g., ===
"active") or the same fallback expression, ensuring consistency between
subscriptionStatus and hasActiveSubscription.

Comment on lines +77 to 80
if (e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
void submitComment();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/comments/CommentInput.tsx | head -100

Repository: pingdotgg/lawn

Length of output: 3391


🏁 Script executed:

rg "isComposing" --type tsx --type ts

Repository: pingdotgg/lawn

Length of output: 85


🏁 Script executed:

rg "handleKeyDown\|KeyboardEvent" src/components/comments/CommentInput.tsx -A 10 -B 5

Repository: pingdotgg/lawn

Length of output: 40


🏁 Script executed:

rg "composing\|composition\|IME" --ignore-case

Repository: pingdotgg/lawn

Length of output: 40


🌐 Web query:

IME composition handling React keydown isComposing best practices

💡 Result:

Best practices for IME-safe onKeyDown in React

  1. Don’t treat Enter (or arrow keys/Esc) as “final” while the user is composing text. During an IME composition session, key events can fire even though the text isn’t finalized yet. Use KeyboardEvent.isComposing to detect this window (between compositionstart and compositionend). [1]

  2. In React, check e.nativeEvent.isComposing in onKeyDown. React wraps the browser KeyboardEvent, so the most reliable flag is usually on the native event. [2]

  3. Safari workaround: Safari can report isComposing === false on the keydown for Enter even though composition is still effectively happening; a common workaround is to also ignore keyCode === 229 (“composition in progress” / “process key”). [2]

  4. For maximum robustness, track composition state explicitly with onCompositionStart/onCompositionEnd and consult that state in onKeyDown (rather than relying only on isComposing). This also aligns with the platform model of composition sessions. [1][3]

React pattern (practical and cross-browser)

function ChatInput({ onSubmit }: { onSubmit: (text: string) => void }) {
  const composingRef = React.useRef(false);
  const inputRef = React.useRef<HTMLInputElement>(null);

  return (
    <input
      ref={inputRef}
      onCompositionStart={() => { composingRef.current = true; }}
      onCompositionEnd={() => { composingRef.current = false; }}
      onKeyDown={(e) => {
        const ne = e.nativeEvent as KeyboardEvent;

        // IME guard (incl. Safari fallback)
        if (composingRef.current || ne.isComposing || ne.keyCode === 229) return;

        if (e.key === "Enter") onSubmit(inputRef.current?.value ?? "");
      }}
    />
  );
}

Rule of thumb: use onInput/onChange to react to text changes, and reserve onKeyDown for shortcuts/submit—but always gate those shortcuts with IME composition checks. [1][2]

Sources: MDN on KeyboardEvent.isComposing [1]; discussion + Safari/229 workaround example [2]; UI Events spec for composition session semantics [3].


🏁 Script executed:

rg "onComposition"

Repository: pingdotgg/lawn

Length of output: 40


🏁 Script executed:

rg "textarea\|TextArea" src/ -l

Repository: pingdotgg/lawn

Length of output: 40


Add IME composition check to prevent submit during text composition.

Line 77 can submit while users are composing text with IME (e.g., Japanese/Chinese/Korean), interrupting input mid-composition.

Add e.nativeEvent.isComposing check before processing Enter:

Suggested fix
   const handleKeyDown = (e: React.KeyboardEvent) => {
+    if (e.nativeEvent.isComposing) return;
     if (e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
       e.preventDefault();
       void submitComment();
     }

Note: For maximum cross-browser robustness (especially Safari), consider also checking e.nativeEvent.keyCode === 229 as a fallback, or explicitly track composition state with onCompositionStart/onCompositionEnd.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
void submitComment();
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.nativeEvent.isComposing) return;
if (e.key === "Enter" && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) {
e.preventDefault();
void submitComment();
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/comments/CommentInput.tsx` around lines 77 - 80, The Enter-key
handler that calls submitComment currently doesn't prevent submission during IME
composition; update the keydown handler (the block checking e.key === "Enter" &&
!e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) to also check for IME
composition by verifying !e.nativeEvent.isComposing before calling
e.preventDefault() and submitComment(), and for broader compatibility add a
fallback that treats nativeEvent.keyCode === 229 as composing (or alternatively
implement onCompositionStart/onCompositionEnd to track a composing flag and
check that flag here); ensure you reference and update the same handler around
the submitComment invocation so composition prevents accidental submits.

Comment on lines +829 to +832
onChange={(e) => {
e.stopPropagation();
setVideoVolume(Number(e.target.value));
}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stop propagation on slider pointer/click events too, not only change.

At Line 829, e.stopPropagation() only runs for onChange. Parent pointerdown/click handlers can still fire while interacting with the slider. Add onPointerDown (and optionally onClick) propagation guards to fully isolate control interaction.

Suggested patch
           <input
             aria-label="Volume"
             type="range"
             min={0}
             max={1}
             step={0.01}
             value={isMuted ? 0 : volume}
+            onPointerDown={(e) => {
+              e.stopPropagation();
+            }}
+            onClick={(e) => {
+              e.stopPropagation();
+            }}
             onChange={(e) => {
               e.stopPropagation();
               setVideoVolume(Number(e.target.value));
             }}
             className="h-1 w-24 cursor-pointer accent-[color:var(--accent)]"
           />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onChange={(e) => {
e.stopPropagation();
setVideoVolume(Number(e.target.value));
}}
<input
aria-label="Volume"
type="range"
min={0}
max={1}
step={0.01}
value={isMuted ? 0 : volume}
onPointerDown={(e) => {
e.stopPropagation();
}}
onClick={(e) => {
e.stopPropagation();
}}
onChange={(e) => {
e.stopPropagation();
setVideoVolume(Number(e.target.value));
}}
className="h-1 w-24 cursor-pointer accent-[color:var(--accent)]"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/video-player/VideoPlayer.tsx` around lines 829 - 832, In
VideoPlayer.tsx the volume slider only calls e.stopPropagation() inside the
onChange handler (where setVideoVolume is called), allowing parent pointer/click
handlers to still fire; add onPointerDown and onClick handlers on the same
slider element that call e.stopPropagation() (and prevent default if needed) to
fully isolate interactions with the control so parent listeners won't receive
pointer or click events when the user manipulates the slider.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant