Skip to content
Merged
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
68 changes: 68 additions & 0 deletions .claude/eslint-rules/prefer-dynamic-import-with-feature-guard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* ESLint rule: prefer-dynamic-import-with-feature-guard
*
* Warns when a file has both static non-framework imports and a browser feature
* detection guard (e.g. `if (!navigator.xxx) return`), which suggests the
* import should be dynamic to avoid loading the module when the feature is unavailable.
*/

const FRAMEWORK_PACKAGES = /^(react|react-dom|next|next-cloudinary|nextra)($|\/)/

module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Prefer dynamic imports when the module is only used inside a browser feature guard.',
},
messages: {
preferDynamic:
'Static import "{{ source }}" may be loaded unnecessarily. ' +
'Consider a dynamic import inside the feature guard (e.g. `if (!navigator.xxx) return`).',
},
},

create(context) {
const projectImports = []
let hasNavigatorGuard = false

function isNavigatorGuard(node) {
// Matches: !navigator.foo or !navigator?.foo
if (node.type !== 'UnaryExpression' || node.operator !== '!') return false
const arg = node.argument
if (arg.type === 'MemberExpression') {
return arg.object?.name === 'navigator'
}
if (arg.type === 'ChainExpression') {
return arg.expression?.object?.name === 'navigator'
}
return false
}

return {
ImportDeclaration(node) {
if (!FRAMEWORK_PACKAGES.test(node.source.value)) {
projectImports.push(node)
}
},

IfStatement(node) {
if (isNavigatorGuard(node.test)) {
hasNavigatorGuard = true
}
},

'Program:exit'() {
if (hasNavigatorGuard && projectImports.length > 0) {
projectImports.forEach((imp) => {
context.report({
node: imp,
messageId: 'preferDynamic',
data: { source: imp.source.value },
})
})
}
},
}
},
}
23 changes: 23 additions & 0 deletions .claude/eslint.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const preferDynamicImport = require('./eslint-rules/prefer-dynamic-import-with-feature-guard.js')

module.exports = [
{
plugins: {
local: {
rules: {
'prefer-dynamic-import-with-feature-guard': preferDynamicImport,
},
},
},
rules: {
'local/prefer-dynamic-import-with-feature-guard': 'warn',
},
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
},
]
37 changes: 37 additions & 0 deletions .claude/hooks/eslint-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
# PostToolUse hook: runs ESLint with the prefer-dynamic-import-with-feature-guard
# rule on any JS/JSX file that Claude just wrote or edited.

INPUT=$(cat)

FILE_PATH=$(echo "$INPUT" | node -e "
process.stdin.setEncoding('utf8')
let data = ''
process.stdin.on('data', chunk => data += chunk)
process.stdin.on('end', () => {
try {
const parsed = JSON.parse(data)
process.stdout.write(parsed.tool_input?.file_path || '')
} catch {}
})
")

# Only lint JS/JSX files
if [[ -z "$FILE_PATH" ]] || [[ ! "$FILE_PATH" =~ \.(js|jsx)$ ]]; then
exit 0
fi

if [[ ! -f "$FILE_PATH" ]]; then
exit 0
fi

CONFIG="$(cd "$(dirname "$0")/.." && pwd)/eslint.config.cjs"

result=$(npx eslint --config "$CONFIG" "$FILE_PATH" 2>&1)

if [[ -n "$result" ]]; then
echo "$result"
exit 1
fi

exit 0
60 changes: 60 additions & 0 deletions components/LazyVideoPlayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useState } from 'react'
import { CldVideoPlayer } from 'next-cloudinary'

export function LazyVideoPlayer({ src, width, height, poster }) {
const [playing, setPlaying] = useState(false)

if (playing) {
return <CldVideoPlayer width={width} height={height} src={src} autoPlay="always" />
}

return (
<div
onClick={() => setPlaying(true)}
style={{
position: 'relative',
cursor: 'pointer',
background: '#000',
aspectRatio: `${width} / ${height}`,
overflow: 'hidden',
}}
>
{poster && (
<img
src={poster}
alt="Video tutorial preview"
width={width}
height={height}
loading="lazy"
style={{ width: '100%', height: 'auto', display: 'block' }}
/>
)}
<div
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div
style={{
width: '64px',
height: '64px',
background: 'rgba(0, 0, 0, 0.65)',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
color: '#fff',
userSelect: 'none',
}}
>
</div>
</div>
</div>
)
}
95 changes: 95 additions & 0 deletions components/WebMCP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useEffect } from 'react'

export function WebMCP() {
useEffect(() => {
if (!navigator.modelContext) return

import(/* webpackChunkName: "snippets-registry" */ '../lib/snippets-registry').then(({ snippets }) => {
navigator.modelContext.registerTool({
name: 'list_snippets',
description:
'List all web performance measurement snippets available on this site. Returns metadata without code.',
inputSchema: { type: 'object', properties: {} },
execute: () =>
snippets.map(({ id, category, title, description, url }) => ({
id,
category,
title,
description,
url,
})),
})

navigator.modelContext.registerTool({
name: 'get_snippet',
description:
'Get the full JavaScript code for a specific web performance snippet by its ID.',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description:
'Snippet ID (e.g. "LCP", "CLS", "TTFB", "INP"). Use list_snippets to discover available IDs.',
},
},
required: ['id'],
},
execute: ({ id }) => {
const snippet = snippets.find((s) => s.id === id)
if (!snippet) {
return { error: `Snippet "${id}" not found. Use list_snippets to see available IDs.` }
}
return snippet
},
})

navigator.modelContext.registerTool({
name: 'search_snippets',
description:
'Search web performance snippets by category and/or keyword. Returns metadata without code.',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description:
'Filter by category. One of: CoreWebVitals, Loading, Interaction, Media, Resources.',
},
query: {
type: 'string',
description: 'Keyword to filter by title or description.',
},
},
},
execute: ({ category, query } = {}) => {
let results = snippets

if (category) {
results = results.filter((s) => s.category === category)
}

if (query) {
const q = query.toLowerCase()
results = results.filter(
(s) =>
s.title.toLowerCase().includes(q) ||
s.description.toLowerCase().includes(q) ||
s.id.toLowerCase().includes(q)
)
}

return results.map(({ id, category, title, description, url }) => ({
id,
category,
title,
description,
url,
}))
},
})
})
}, [])

return null
}
Loading