chore: enforce Vue style and state boundaries

This commit is contained in:
Danila Poyarkov 2026-05-14 22:05:43 +03:00
parent 3e6093106c
commit 686ca5326f
7 changed files with 55 additions and 30 deletions

View file

@ -238,7 +238,7 @@ Release commits are the exception: keep using `Release v0.x.y`.
## Code conventions
- Do not place code or tests ad hoc. Before adding or moving files, inspect the existing folder structure and nearby patterns, then put changes in the established domain-specific location. If no proper location exists, create one deliberately and update docs/conventions as needed.
- Architecture boundaries are enforced by Steiger (`bun run check:arch`). App code must use public workspace package exports, workspace packages must not import app `src/` code, package-local aliases (`#core`, `#vue`, `#cli`, `#mcp`) are only for their owning package, core must stay framework-agnostic, app service/domain code (`src/app/**`) must not import app component/view layers, components must not import views, shared UI (`src/components/ui/**`) must not import app services/stores, property-panel internals must stay inside the property panel, canvas/editor overlay code must not import property-panel internals, committed code must not import scratch/generated/vendor internals, and durable docs belong under `packages/docs/**` unless the root Markdown allowlist is deliberately updated.
- Architecture boundaries are enforced by Steiger (`bun run check:arch`). App code must use public workspace package exports, workspace packages must not import app `src/` code, package-local aliases (`#core`, `#vue`, `#cli`, `#mcp`) are only for their owning package, core must stay framework-agnostic, app service/domain code (`src/app/**`) must not import app component/view layers, components must not import views, shared UI (`src/components/ui/**`) must not import app services/stores, property-panel internals must stay inside the property panel, canvas/editor overlay code must not import property-panel internals, Vue components must not use `<style>` blocks, code outside core editor internals must not assign `editor.state.selectedIds` or `editor.state.activeTool` directly, committed code must not import scratch/generated/vendor internals, and durable docs belong under `packages/docs/**` unless the root Markdown allowlist is deliberately updated.
- Test placement is strict and enforced by Steiger: app E2E tests live under `tests/e2e/**` and use `*.spec.ts`; Figma automation tests live under `tests/figma/**` and use `*.spec.ts`; engine/unit tests live under `tests/engine/**` and use `*.test.ts` (with `helpers.ts`, `*.bench.ts`, and `visual-*` support scripts allowed); shared test utilities live under `tests/helpers/**`. Do not commit temporary/profile specs (`*.tmp.*`, `*.profile.*`). Do not put store-only/internal-state assertions in E2E. If a test drives the UI like a user and verifies visible behavior, it can be E2E; if it creates nodes through internals and asserts graph state, it belongs in engine/unit coverage.
### File and folder naming

View file

@ -230,6 +230,27 @@ const noE2EImportsInEngineTests = createImportRule(
}
)
const noVueStyleBlocks = createFileRule('open-pencil/no-vue-style-blocks', (sourceRel) => {
if (!sourceRel.endsWith('.vue')) return null
if (!sourceRel.startsWith('src/') && !sourceRel.startsWith('packages/vue/src/')) return null
const content = readFileSync(path.resolve(sourceRel), 'utf8')
return /<style\b/i.test(content)
? 'Vue components must not use <style> blocks. Use Tailwind utilities or global app.css tokens.'
: null
})
const noDirectSelectionOrToolStateMutation = createFileRule(
'open-pencil/no-direct-selection-tool-state-mutation',
(sourceRel) => {
if (!sourceRel.endsWith('.ts') && !sourceRel.endsWith('.vue')) return null
if (sourceRel.startsWith('packages/core/src/editor/')) return null
const content = readFileSync(path.resolve(sourceRel), 'utf8')
return /\.state\.(selectedIds|activeTool)\s*=(?!=)/.test(content)
? 'Do not assign editor.state.selectedIds or editor.state.activeTool directly. Use editor selection/tool actions.'
: null
}
)
const noRootMarkdownClutter = createFileRule('open-pencil/no-root-markdown-clutter', (sourceRel) => {
if (sourceRel.includes('/')) return null
if (!sourceRel.endsWith('.md')) return null
@ -397,6 +418,8 @@ export const openPencilArchitecturePlugin = {
meta: { name: 'open-pencil-architecture', version: '0.0.0' },
ruleDefinitions: [
preferDomainFoldersOverFilenamePrefixes,
noVueStyleBlocks,
noDirectSelectionOrToolStateMutation,
strictTestFilePlacement,
noEngineOnlyAssertionsInE2E,
noE2EImportsInEngineTests,

View file

@ -109,6 +109,32 @@ body {
-webkit-user-select: none;
}
.code-highlight .token.tag {
color: var(--color-code-tag);
}
.code-highlight .token.attr-name {
color: var(--color-code-attribute);
}
.code-highlight .token.attr-value,
.code-highlight .token.string {
color: var(--color-code-string);
}
.code-highlight .token.number,
.code-highlight .token.boolean {
color: var(--color-code-number);
}
.code-highlight .token.punctuation {
color: var(--color-code-punctuation);
}
.code-highlight .token.keyword {
color: var(--color-code-attribute);
}
input[type='number'] {
appearance: textfield;
-moz-appearance: textfield;

View file

@ -92,7 +92,7 @@ function copyReference() {
</div>
<ScrollAreaRoot class="min-h-0 flex-1">
<ScrollAreaViewport class="size-full">
<ScrollAreaViewport class="code-highlight size-full">
<div class="p-3">
<div v-for="(html, i) in highlightedLines" :key="i" class="flex text-xs leading-5">
<span
@ -113,27 +113,3 @@ function copyReference() {
</div>
</template>
<style scoped>
.token.tag {
color: var(--color-code-tag);
}
.token.attr-name {
color: var(--color-code-attribute);
}
.token.attr-value,
.token.string {
color: var(--color-code-string);
}
.token.number {
color: var(--color-code-number);
}
.token.punctuation {
color: var(--color-code-punctuation);
}
.token.boolean {
color: var(--color-code-number);
}
.token.keyword {
color: var(--color-code-attribute);
}
</style>

View file

@ -22,6 +22,8 @@ export default defineConfig([
{
rules: {
'open-pencil/prefer-domain-folders-over-filename-prefixes': 'error',
'open-pencil/no-vue-style-blocks': 'error',
'open-pencil/no-direct-selection-tool-state-mutation': 'error',
'open-pencil/strict-test-file-placement': 'error',
'open-pencil/no-engine-only-assertions-in-e2e': 'error',
'open-pencil/no-e2e-imports-in-engine-tests': 'error',

View file

@ -39,8 +39,7 @@ async function selectDemoCard(page: Parameters<typeof test>[0]['page'], canvas:
.map((n) => `${n.name}:${n.type}`)
.join(', ')}`
)
store.state.selectedIds = new Set([card.id])
store.requestRender()
store.select([card.id])
})
await canvas.waitForRender()

View file

@ -103,8 +103,7 @@ test('stroke picker hsb saturation and brightness sliders update stroke color on
align: 'INSIDE'
}
store.updateNodeWithUndo(card.id, { strokes: [stroke] }, 'Add demo card stroke')
store.state.selectedIds = new Set([card.id])
store.requestRender()
store.select([card.id])
})
await canvas.waitForRender()