Skip to content

Coding Conventions -- Detailed Reference ​

Terse rules auto-loaded from .claude/rules/coding-conventions.md. This file has detailed examples and rationale.


i18n: All user-visible strings via $t() ​

Every string a user can see must use an i18n key. This includes templates, JS fallbacks, and dynamically built menus.

vue
<!-- BAD -->
<span>Add target</span>

<!-- GOOD -->
<span>{{ $t('visuals.animation.addTarget') }}</span>
js
// BAD -- hard-coded fallback in script
if (!blockName) return 'No target'

// GOOD
if (!blockName) return this.$t('visuals.animation.noTarget')
js
// BAD -- hard-coded label in dynamic menu
{ label: 'No animation', value: '' }

// GOOD
{ label: this.$t('visuals.animation.noAnimation'), value: '' }

Exception: CSS technical terms (Ease, Ease In, Linear) are universal -- no i18n needed.

Key location: src/assets/i18n/en.js -- nest under relevant section (e.g., visuals.animation.*).


inputLocked: Propagate to all mutating children ​

When a parent receives inputLocked (e.g., child creative without overrides), every child that allows mutation must be disabled or hidden.

vue
<!-- BAD -- EffectList can still be edited when locked -->
<AnimationEffectList :effects="config.effects" />

<!-- GOOD -->
<AnimationEffectList :effects="config.effects" :disabled="inputLocked" />
vue
<!-- BAD -- paste button visible when locked -->
<TooltipWrapper v-if="canPaste">

<!-- GOOD -->
<TooltipWrapper v-if="canPaste && !inputLocked">

Read-only actions (copy, preview) don't need gating.


InputSelect for dropdowns ​

Always use InputSelect for dropdowns in builder configuration panels. Never native <select>.

vue
<!-- BAD -->
<select v-model="alignment">
  <option value="left">Left</option>
  <option value="center">Center</option>
</select>

<!-- GOOD -->
<InputSelect
  :items="alignmentOptions"
  :value="alignment"
  @change="updateAlignment"
/>

InputSelect provides consistent styling, theming support, and keyboard navigation.


sectionSettings registration ​

Every new Section component using sectionLogic must have an entry in utils.ts. Without it, getSectionSettings() will crash at runtime.

ts
// In Configuration/components/utils.ts
export const sectionSettings = {
  // ...existing entries
  MyNewSection: {
    overridePaths: ['style.myProperty'],
  },
}

OptionList: Use full-width for overflow ​

When an OptionList has more than 3-4 items or items with long labels, add the full-width prop to prevent horizontal overflow.

vue
<!-- BAD -- 6 items will overflow the default width -->
<OptionList :items="animationPresets" :value="selected" />

<!-- GOOD -->
<OptionList :items="animationPresets" :value="selected" full-width />

Component reuse: Extend before you create ​

The codebase has a rich library of reusable components. Always search for existing solutions before building new UI.

Prefer extending existing components ​

vue
<!-- BAD -- new DropdownMenu component that duplicates InputSelect behavior -->
<DropdownMenu :items="presets" @select="onSelect" />

<!-- GOOD -- reuse existing InputSelect with a prop tweak -->
<InputSelect :items="presets" :value="selected" @change="onSelect" />

If an existing component almost does what you need, add a prop or slot to it rather than creating a parallel component.

Don't use raw DOM when Vue components exist ​

js
// BAD -- building a dropdown with document.createElement in a Vue component
const menu = document.createElement('div')
items.forEach(item => {
  const btn = document.createElement('button')
  btn.textContent = item.label
  menu.appendChild(btn)
})
document.body.appendChild(menu)

// GOOD -- use an existing component or create a minimal Vue one
// Floating menus: use Vuetify's v-menu, or the existing preset-menu pattern

Raw DOM creation inside Vue components breaks reactivity, doesn't respect scoped styles, can leak event listeners, and is harder to maintain.

When to extract a new component ​

Extract when the same pattern appears in 2+ places. Don't pre-extract "just in case."

1 usage  -> inline it
2 usages -> extract a component
3 usages -> the component was the right call

When extracting, prefer making it generic enough for its known use cases, but no more. Don't add configurability for hypothetical future needs.


Data-driven templates: v-for over repeated sibling elements ​

When 3+ sibling elements share the same structure (buttons, list items, toolbar actions), define them as a data or computed array and render with v-for.

vue
<!-- BAD -- four near-identical buttons -->
<button :class="{ active: editor.isActive('italic') }" class="btn italic"
  @mousedown.prevent="editor.chain().focus().toggleItalic().run()">I</button>
<button :class="{ active: editor.isActive('underline') }" class="btn"
  @mousedown.prevent="editor.chain().focus().toggleUnderline().run()">U</button>
<button :class="{ active: editor.isActive('strike') }" class="btn strike"
  @mousedown.prevent="editor.chain().focus().toggleStrike().run()">S</button>
<button :class="{ active: isUppercase }" class="btn uppercase"
  @mousedown.prevent="toggleUppercase">TT</button>

<!-- GOOD -- data-driven with v-for -->
<button
  v-for="btn in formattingButtons"
  :key="btn.label"
  :class="[{ active: btn.active }, 'btn', btn.cssClass]"
  @mousedown.prevent="btn.action"
>
  {{ btn.label }}
</button>
ts
// In computed:
formattingButtons() {
  return [
    { label: 'I', cssClass: 'italic', active: this.editor.isActive('italic'),
      action: () => this.editor.chain().focus().toggleItalic().run() },
    { label: 'U', cssClass: '', active: this.editor.isActive('underline'),
      action: () => this.editor.chain().focus().toggleUnderline().run() },
    // ...
  ]
}

Keep individual elements only when they are structurally unique (e.g., a color picker with a slot, a dropdown with a sub-menu). See AddBlockTool.vue and RichTextEditor.vue for real examples.


Arrow functions vs function declarations ​

Prefer arrow functions for standalone/utility functions. function declarations are fine when you need hoisting (mutual recursion, call-before-definition). Vue component methods use object method shorthand naturally.

ts
// GOOD -- standalone utility
const isVideo = (p) => p.type === 'video'

// GOOD -- needs hoisting (mutual recursion)
function parseNode(node) {
  node.children.forEach(parseChild)
}
function parseChild(child) {
  if (child.nested) parseNode(child)
}

// GOOD -- Vue component method (object shorthand)
methods: {
  handleClick() {
    this.$emit('click')
  },
}

Code style: Breathing room and early returns ​

Breathing room ​

Write code with blank lines between logical blocks. Never collapse if/guard clauses onto one line.

ts
// BAD
if (!value) return
const result = compute()
if (result.error) return handleError(result)
return result.data

// GOOD
if (!value) {
  return
}

const result = compute()

if (result.error) {
  return handleError(result)
}

return result.data

Early returns over deep nesting ​

Handle edge cases and bail out at the top of a function.

ts
// BAD -- deeply nested
const processBlock = (block) => {
  if (block) {
    if (block.visible) {
      if (block.type === 'text') {
        return formatText(block)
      }
    }
  }
  return null
}

// GOOD -- flat with early returns
const processBlock = (block) => {
  if (!block || !block.visible) {
    return null
  }

  if (block.type !== 'text') {
    return null
  }

  return formatText(block)
}

Control flow: switch and while ​

switch over if-chains ​

When multiple branches check the same variable, use switch. It signals "one value, many outcomes" and encourages extracting logic per case.

ts
// BAD -- if-chain checking the same variable
if (block.type === 'text') {
  return renderText(block)
} else if (block.type === 'button') {
  return renderButton(block)
} else if (block.type === 'graphic') {
  return renderGraphic(block)
} else {
  return renderDefault(block)
}

// GOOD
switch (block.type) {
  case 'text':
    return renderText(block)
  case 'button':
    return renderButton(block)
  case 'graphic':
    return renderGraphic(block)
  default:
    return renderDefault(block)
}

while/do-while for traversal ​

Use while when the number of iterations is unknown (flow walking, linked lists, tree traversal). It expresses "repeat until done" more clearly than a for loop with break conditions.

ts
// BAD -- for loop with awkward break
for (let i = 0; ; i++) {
  const node = getNext(current)
  if (!node) break
  current = node
}

// GOOD
while (current) {
  current = getNext(current)
}

Constants: Extract repeated hardcoded values ​

Extract hardcoded values (URLs, regex patterns, magic numbers) to named constants when they appear in 2+ files.

ts
// BAD -- magic number in multiple files
if (tagline.offsetWidth > 80) { ... }
// ...in another file:
const maxIconSize = Math.min(parsedSize, 80)

// GOOD -- named constant
const MAX_ICON_SIZE = 80

Single-file magic numbers are fine if their meaning is obvious from context. The rule targets cross-file duplication.


Section comments for long files ​

Use section comments (// -- Section Name --) to divide longer files into navigable chunks.

ts
// -- Computed --

const blockCount = computed(() => blocks.value.length)

// -- Methods --

const addBlock = (type: string) => { ... }

// -- Watchers --

watch(selectedBlock, (block) => { ... })

This is especially useful in large Vue components where the Options API groups by option type, and in utility files with many exports.


JSDoc: Explain why, not what ​

Add JSDoc on non-obvious functions where it aids understanding. Skip it on self-evident code.

ts
// BAD -- restates the code
/** Returns true if the block is a video block */
const isVideo = (block) => block.type === 'video'

// GOOD -- explains a non-obvious decision
/**
 * Strips bare <span> wrappers that Tiptap inserts when removing
 * the last text-style attribute. Without this, the HTML accumulates
 * empty spans on every edit cycle.
 */
const stripEmptySpans = (html: string): string =>
  html.replace(/<span\s*>(.*?)<\/span>/g, '$1')

Include type info when the signature alone is ambiguous, and examples when the behavior is non-trivial.


TypeScript conventions ​

type over interface ​

Use type instead of interface. They're more versatile (unions, intersections, mapped types) and consistent across the codebase.

ts
// BAD
interface BlockConfig {
  name: string
  visible: boolean
}

// GOOD
type BlockConfig = {
  name: string
  visible: boolean
}

Type locations ​

  • Shared types go in src/interfaces/ for global use
  • Domain-specific types go in colocated types.ts companion files (e.g., Blocks/data/types.ts)
  • Never define types inside .vue files -- extract them

Type judiciously ​

Add types where they clarify. Skip them where the code is self-evident.

ts
// BAD -- type adds nothing, the filter + boolean return is obvious
const isVideo = (p: BlockProperties): boolean => p.type === 'video'

// GOOD -- no annotation needed
const isVideo = (p) => p.type === 'video'

// GOOD -- return type clarifies what a complex function produces
const buildParamValues = (params, saved): Record<string, unknown> => { ... }

Utils purity ​

Keep src/utils/ pure. No Vue reactivity (ref, computed, watch), no side effects, no store access. Utils should be plain functions that take inputs and return outputs.

ts
// BAD -- reactive import in utils
import { ref } from 'vue'
import store from '@/store'

export const getActiveBlock = () => store.state.blocks.selectedBlock

// GOOD -- pure function, caller provides data
export const getBlockPath = (blockName: string, parentPath?: string): string => {
  if (!parentPath) {
    return blockName
  }

  return `${parentPath}.${blockName}`
}

If you need reactivity, put it in a composable (src/composables/) or a Vuex module.


Naming: Readability over brevity ​

Avoid abbreviations when a descriptive name is just as short.

ts
// BAD
const dict = {}
const val = getVal()
const cb = () => {}
const idx = items.findIndex(...)

// GOOD
const blockNameTypes = {}
const value = getValue()
const onChange = () => {}
const index = items.findIndex(...)

Well-known abbreviations are fine: el (element), i/j (loop counters), fn (when the variable literally holds "a function"), e (event in handlers).


Refactor as you go ​

When touching existing code for a feature or bugfix, clean up what you touch:

  • Expand one-liner guards to multi-line with braces
  • Add breathing room (blank lines between logical blocks)
  • Convert function to arrow where appropriate
  • Look at related code nearby for similar improvements

Don't refactor unrelated code in the same PR, but leave the code you change cleaner than you found it.

ts
// Before (touching this file for a bug fix):
if (!block) return
const name = block.name
if (name === 'text') return handleText(block)

// After (cleaned up while fixing the bug):
if (!block) {
  return
}

const { name } = block

if (name === 'text') {
  return handleText(block)
}

Before committing ​

Never commit or push until the user has completed all of the following:

  1. Review the diff -- does the code read well, follow conventions, and avoid unnecessary complexity?
  2. Open and save changed files to trigger lint
  3. Test locally -- check for console errors, verify in builder, flow, delivery standalone preview, and confirm it renders correctly on external sites (e.g. test-a-tag)
  4. Explicitly confirm that everything looks good

Internal documentation