From 5acb8ffb6c0bb76aeeea14e4b6d285c3cb9222bf Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 14 May 2026 19:00:51 +0300 Subject: [PATCH] chore: guard shared UI boundaries --- AGENTS.md | 2 +- scripts/steiger-rules.ts | 23 +++++++++++++++++++++++ src/app/shell/ui.ts | 3 ++- src/components/ui/toast.ts | 2 +- steiger.config.ts | 2 ++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2d6c98ebc..a26f7423f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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, and canvas/editor overlay code must not import property-panel internals. +- 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, shared UI (`src/components/ui/**`) must not import app services/stores, property-panel internals must stay inside the property panel, and canvas/editor overlay code must not import property-panel internals. - Test placement is strict: E2E tests live under `tests/e2e/**` and use `*.spec.ts`; engine/unit tests live under `tests/engine/**` and use `*.test.ts`. 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 diff --git a/scripts/steiger-rules.ts b/scripts/steiger-rules.ts index ab9898144..7b0cd7904 100644 --- a/scripts/steiger-rules.ts +++ b/scripts/steiger-rules.ts @@ -168,6 +168,27 @@ const noForeignPackageLocalAliases = createImportRule( } ) +const noAppImportsInSharedUi = createImportRule( + 'open-pencil/no-app-imports-in-shared-ui', + (sourceRel, _specifier, resolved) => { + if (!sourceRel.startsWith('src/components/ui/')) return null + if (resolved?.startsWith('src/app/')) { + return 'Shared UI components must not import app services or stores. Pass data/actions in or move app-specific wrappers outside src/components/ui.' + } + return null + } +) + +const noPropertyPanelInternalsOutsidePanel = createImportRule( + 'open-pencil/no-property-panel-internals-outside-panel', + (sourceRel, _specifier, resolved) => { + if (!resolved?.startsWith('src/components/properties/')) return null + if (sourceRel.startsWith('src/components/properties/')) return null + if (sourceRel === 'src/components/DesignPanel.vue') return null + return 'Property-panel internals must stay inside the property panel. Extract app-neutral UI before reusing elsewhere.' + } +) + const noUiImportsInCore = createImportRule( 'open-pencil/no-ui-imports-in-core', (sourceRel, specifier) => { @@ -192,6 +213,8 @@ export const openPencilArchitecturePlugin = { noAppImportsInWorkspacePackages, noPackageInternalsInApp, noForeignPackageLocalAliases, + noAppImportsInSharedUi, + noPropertyPanelInternalsOutsidePanel, noUiImportsInCore ] } diff --git a/src/app/shell/ui.ts b/src/app/shell/ui.ts index 4d4f93ca7..d3225d897 100644 --- a/src/app/shell/ui.ts +++ b/src/app/shell/ui.ts @@ -2,8 +2,9 @@ import { useEventListener } from '@vueuse/core' import { ref } from 'vue' import { isTauri } from '@/app/tauri/env' +import type { ToastVariant } from '@/components/ui/toast' -export type ToastVariant = 'default' | 'warning' | 'error' +export type { ToastVariant } from '@/components/ui/toast' export interface Toast { id: number diff --git a/src/components/ui/toast.ts b/src/components/ui/toast.ts index faeb6f9aa..7e0778c0e 100644 --- a/src/components/ui/toast.ts +++ b/src/components/ui/toast.ts @@ -1,7 +1,7 @@ import { twMerge } from 'tailwind-merge' import { tv } from 'tailwind-variants' -import type { ToastVariant } from '@/app/shell/ui' +export type ToastVariant = 'default' | 'warning' | 'error' const toast = tv({ base: 'flex max-w-sm items-start gap-1.5 rounded-md px-2.5 py-1.5 text-xs shadow-md data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-1 data-[state=open]:animate-in data-[state=open]:fade-in data-[state=open]:slide-in-from-top-1 data-[swipe=cancel]:translate-y-0 data-[swipe=cancel]:transition-transform data-[swipe=move]:translate-y-[var(--reka-toast-swipe-move-y)]', diff --git a/steiger.config.ts b/steiger.config.ts index 62eb8a6f2..1d6fb5fe8 100644 --- a/steiger.config.ts +++ b/steiger.config.ts @@ -23,6 +23,8 @@ export default defineConfig([ 'open-pencil/no-app-imports-in-workspace-packages': 'error', 'open-pencil/no-package-internals-in-app': 'error', 'open-pencil/no-foreign-package-local-aliases': 'error', + 'open-pencil/no-app-imports-in-shared-ui': 'error', + 'open-pencil/no-property-panel-internals-outside-panel': 'error', 'open-pencil/no-ui-imports-in-core': 'error' } }