Skip to content

Vue

Next release

How midcode runs a Vite + Vue app, marks the elements of every template, and what you can edit in a .vue file, from text and classes to the props of each component.

View as Markdown

This ships with the next release of midcode. The version you can download today (1.1.2) doesn’t have it yet.

midcode edits a Vue app built with Vite in the <template> of its single-file components: text, classes, attributes, new elements, their order, and the props a component is given where it’s used. Your <script>, your <style> and everything the template computes ({{ }}, :bound values, v-if) stay exactly as you wrote them. midcode adds to a <script setup> in two cases only: the import of a component you drag in from the Assets tab, and the variant prop of a component you give variants. midcode 1.1.2 and earlier open a Vue project to check every breakpoint and leave comments, without editing.

A Vue app built with Vue CLI runs another way: see Create React App, Vue CLI, Gatsby, Docusaurus. Nuxt has its own page, and so does Vue inside Laravel.

At a glance

Vue with Vite
Detected byvite and vue in the dependencies of package.json
Runs withYour project’s own Vite, with your config and one plugin added
Elements are marked byThat plugin, in memory, while the dev server runs
EditingText, classes, attributes, tag; insert, move, duplicate, delete, wrap in a stack
ComponentsInstances and typed props. A component opens on its own canvas, with variants, states and variables
PagesThe home page is listed; any other route opens by typing its path
StylesYour Tailwind 4 classes, or mid: utilities compiled to midcode.css
Tried withVite + Vue + Tailwind 4

How midcode runs it

midcode takes a project for Vue with Vite when its package.json lists vite and vue and none of the frameworks it checks first (Next.js, React Router, Remix, Astro, SvelteKit, Nuxt). It then starts the Vite that’s in your node_modules:

Terminal
vite dev <your project> --config <midcode's config> --port 4310 --strictPort --host localhost

The port is the first free one from 4310. It runs with the Node your shell has, or with midcode’s own when there is none. If your dev script gives Vite other options (vite --mode staging), midcode passes them along; the port, the host and the config are midcode’s to say.

That config is a small file in midcode’s own data folder (~/Library/Application Support/midcode/injected/vite.config.mjs), not in your project. It loads your vite.config with your project’s Vite and puts one plugin first in the list. Your vite.config, your package.json and your lockfile aren’t changed, and the plugin only applies to the dev server, never to a build.

As Vite loads each .vue file, the plugin adds a mark to every element of its <template> before Vue compiles it. This file:

src/components/Hero.vue
<template>
  <section class="hero">
    <h1 class="title">Build calmly</h1>
    <Card title="Oak" :count="3" />
  </section>
</template>

reaches Vue’s compiler like this (the root’s data-mcu left out):

Vue
<template>
  <section data-mc="src/components/Hero.vue:2:3" class="hero">
    <h1 data-mc="src/components/Hero.vue:3:5" class="title">Build calmly</h1>
    <Card data-mci="Card|src/components/Hero.vue:4:5" title="Oak" :count="3" />
  </section>
</template>
  • data-mc is the file, line and column where the element is written. It’s what the canvas edits by.

  • data-mci goes on a component used in a template. Vue hands it to the component’s root element, like any attribute the component doesn’t declare.

  • data-mcu goes on the elements at the top of each template, read from $attrs. It says where this component is used, also when the component has several roots.

Tags that draw nothing of their own get no instance mark: <template>, <slot>, <component>, <transition>, <transition-group>, <keep-alive>, <teleport>, <suspense>, <router-view>.

None of this is written to disk. Hot reload keeps working: when a file changes, Vue’s plugin receives the marked version again.

If midcode’s way of starting Vite doesn’t come up (the process stops, or nothing answers in 45 seconds), midcode runs your own dev script instead and says so. The site shows, but without marks, so you can only view and comment. Open a project covers that case.

What you can edit

Click an element and the right panel shows where it’s written, as file:line. The editor’s own pages describe each tool: select, move and resize, text and media, the style panel, Insert, Layers. What follows is how each one lands in a .vue file.

Text

Double-click a text and type. The new words replace the old ones in the template:

src/components/Hero.vue
-    <h1 class="title">Build calmly</h1>+    <h1 class="title">Build slowly</h1>

A text is edited there when everything inside its element is written text. If you type {{, it’s written as an entity (&#123;&#123;), so Vue doesn’t read it as an expression.

Text that comes from {{ title }} isn’t in the template. midcode then looks for the exact string in the project’s .ts, .js, .json and .md files (a data file, a locale) and edits it there when it can tell which one it is. .vue files aren’t searched: a value written in the component’s own <script> is edited in the code, and one given as a prop where the component is used is changed there, in the code or from the instance’s Props. Text, images and video explains the search.

Classes and attributes

The style panel writes into the static class attribute, and creates it when the element has none. A :class binding is an expression and is left alone:

src/components/Hero.vue
-    <h1 class="title" :class="{ dark }">Build calmly</h1>+    <h1 class="title text-6xl" :class="{ dark }">Build calmly</h1>

If a class you’re replacing isn’t written in the template, midcode says it comes from a prop or a condition and writes nothing.

Attributes (a link’s href, an image’s src and alt, an input’s settings, a <select> and its options) are read as written, including the ones Vue binds to code. A value is written the way Vue writes it:

ValueWritten as
A texthref="/pricing"
Ondisabled
A number or other code:maxlength="40"

Structure

Insert adds plain markup (what the panel holds is converted from JSX: class, not className). A component dragged from the Assets tab is written with its import, in the file’s <script setup> (one is added at the top of the file when it has none), as a relative path with its extension: import Card from './Card.vue'. Dragging moves an element before, after or inside another one in the same file, and the comment right above it travels with it. Several elements can be deleted, moved or wrapped in a stack (⇧A) at once. ⌥-drag leaves the element where it is and puts a copy beside or inside the one you drop it on, in the same file.

Components and props

A component used in a template shows in Layers with its name and is selected as an instance. Its Props in the right panel are typed controls, read from what the component declares:

  • defineProps<Props>(), with the defaults from withDefaults(…, { … }) or from a destructuring.

  • defineProps({ title: String, count: { type: Number, default: 0 } }). String, Number and Boolean choose the control, and required: true is respected.

  • defineProps(['title', 'image']): the names alone.

  • The props of the object a plain <script> exports.

src/components/Card.vue
<script setup lang="ts">
interface Props {
  title: string
  count?: number
  featured?: boolean
}
const props = withDefaults(defineProps<Props>(), { count: 0, featured: false })
</script>

Changing a prop writes it where the component is used:

src/components/Hero.vue
-    <Card title="Oak" :count="3" />+    <Card title="Walnut" :count="5" featured />

A value equal to the declared default removes the attribute. A new one goes after the last attribute, or before a v-bind="…" when there is one. <card-item> is read as CardItem, and my-title as the prop myTitle. A component used without an import (unplugin-vue-components) is looked for by name under components/, app/components/, src/components/ and layers/.

Variants and states

A .vue file is a component. The Assets tab lists it (pages, layouts and App.vue aside), and it opens on its own canvas, where you add variants and states: see Components.

A state is written as classes (hover:, active:, focus-visible:), as in any component. A variant is a variant prop, declared the way the component already declares its props, with :data-variant="variant" and a group/<name> class on each root of the template:

The component hasThe prop is written as
defineProps<Props>()A variant?: 'primary' | 'ghost' member in the type, and its default in withDefaults(…) (put around defineProps when it isn’t there) or in the destructuring
defineProps({ … })variant: { type: String, default: 'primary' }, with the list of variants in a JSDoc @type above it
No props yetOne line: withDefaults(defineProps<{ variant?: 'primary' | 'ghost' }>(), { variant: 'primary' }) in TypeScript, the defineProps({ … }) form in JavaScript. A <script setup> is added when the file has none

Renaming or removing a variant follows the instances that set it (variant="ghost", :variant="'ghost'").

Pages

midcode doesn’t read Vue Router’s routes, so the page menu in the top bar lists only the home page. To open another one, type its path there (“Search, or type a path and press Enter”). Pages and navigation has the rest. midcode doesn’t add pages to a Vue app: its routes are code, yours or your agent’s to write.

Site settings

The globe in the top bar opens Site settings. The title, the description, the language, search engines, the favicon and the social image are written as tags in the <head> of your index.html:

index.html
-    <title>Vite + Vue</title>+    <title>Oak Studio</title>+    <meta name="description" content="Furniture made to last." />   </head>

Images are copied into public/. A value Vite fills in when it builds (%VITE_APP_TITLE%) shows as code and isn’t written. There’s one form for the whole app: a page has no <head> of its own.

Styles

With Tailwind 4 (a stylesheet with @import "tailwindcss" or an @theme block), the panel writes your own utilities: see Tailwind CSS.

Without it, midcode writes its own prefixed utilities (class="title mid:text-[56px]") and keeps their plain CSS in midcode.css, next to the module your index.html loads. The first style edit creates the file and imports it there, as one undoable step:

src/main.ts
 import { createApp } from 'vue' import App from './App.vue'+import './midcode.css'

Your <style scoped> blocks keep working and aren’t edited. Without Tailwind has the details.

Limits

  • A <template lang="pug"> isn’t read: its elements get no marks.

  • Elements move within one file. midcode doesn’t follow what a v-for gives its contents: an element that reads the loop’s item can be dragged out of the loop, where that name no longer exists. ⌘Z puts it back.

  • An insert that needs an import is refused in a component written with the Options API (a plain <script> and no <script setup>), where an import alone wouldn’t be enough: “This needs an import, which midcode can’t add to Hero.vue yet. Add it in the code.” Insert’s interactive components are React and answer “Needs a React page”; icons go in as inline SVG.

  • variables are written as {{ title }}, :src="image" and a :style="{ … }" object. A prop the template or the script also reads in its own code isn’t renamed or removed from the canvas.

  • Variants aren’t written for a component on the Options API or one that lists its props as defineProps([...]) (“Variants for a component that declares its props this way are coming”), nor for one whose template or script already reads variant (“This component’s variants are written in code: edit them there or ask your agent”).

  • In the released version there’s no free canvas. The next release brings the free canvas to a Vite + Vue app, for plain elements: a component or an element with a binding in it can’t float.

  • Tried: text, classes, attributes and structure in a Vite + Vue + Tailwind 4 app; props typed by an interface and by defineProps({ … }), read and written; a component with two roots. Removing, wrapping and moving several elements, and Site settings, were tried on Vite + Vue projects too. So was component mode, in TypeScript and in JavaScript: variants read, added, renamed and removed, and a style written in one variant’s cell of the grid. A Vue app without Tailwind follows the same path as any Vite project, but wasn’t tried with Vue itself.