# MVFrame

**English** | [简体中文](README.cn.md)

A Vue 3 + Vite oriented admin-shell toolkit: **Frame layout**, **common business components**, **global utilities & directives**, **Pinia wiring with tabs/router**, **composable modules**, and **SCSS utilities & tokens**. Priorities favor SaaS-style admin apps over a fully pluggable, zero-opinion utility library.

---

## Stack & dependencies

| Kind | Notes |
|------|------|
| Runtime | `vue` ^3.3, `vue-router` ^4.6, `pinia` ^3 |
| Demo / dev | `element-plus` (demo forms, etc.), `vite` ^6, `sass-embedded` |
| Build | Library output is **ESM**; **`vue` / `pinia` / `vue-router` are externals** and must be supplied by the host app |

`package.json` exports include:

- `"."` → `dist/index.js` (main plugin; default export is the `install` function)
- `"./composition"` → `dist/composition.js` (e.g. `import { useMap } from 'mvframe/composition'`)
- `"./store"`, `"./directive"`, `"./util"` → matching chunks under `dist`
- `"./notify"` → `dist/notify.js` (browser helper for the local `mvframe-notify` service)
- `"./style/mixin"` → `src/style/chip/mixin.scss` (shared breakpoint and helper mixins for host SCSS)
- `"./style"` → `dist/css/style.css` (full toolkit CSS from `src/style/index.scss` entry)
- `"./style/cpt"` → `dist/css/cpt.css` (component-extracted CSS; usually `import 'mvframe/style'` is enough)

---

## Repository layout

### `src/` — library source

| Path | Role |
|------|------|
| `src/index.js` | **Install entry**: registers `config` → global components (`cpt`) → `util` → `directive` → `store` → `router` |
| `src/config/` | Merges defaults with app options → `globalThis.$config` (`chip/base.js`: name, copyright, version, etc.) |
| `src/cpt/index.js` | **Auto-registers** every `component/*/index.vue` via `import.meta.glob`; component name equals the **folder** name (PascalCase, e.g. `BtnGroup`) |
| `src/component/` | Implementations; root class `Mvc` + name (see `.cursor/rules/component-hierarchy.mdc`) |
| `src/util/index.js` | Global helpers on `app.config.globalProperties` & `globalThis` (`$getLang`, `$fa`, `$copy`, `$deepClone`, …) |
| `src/directive/index.js` | Directives: `v-copy` (optional `.dblclick`), `v-focus` |
| `src/store/` | `createPinia` + built-in `init` / `tab` / `rmenu`; optional `storeChips`; `provide("store", store)` |
| `src/router/` | `createWebHistory` router; optional `guard`; built-in `guardThis` (`meta.admin`, `afterEach` + tabs, `document.title`) |
| `src/composition/` | **Import on demand**; exports `lang`, `dom`, `data`, `media`, … (Vite alias `@cps` → this folder) |
| `src/style/` | `index.scss` aggregates `chip/*.scss` (tokens, spacing, typography, layout, overrides) |

**Globally registered components** (folder name = registration name):

`BtnGroup`, `Form`, `Frame`, `Icon`, `Input`, `Page`, `Select`, `SelectV2`, `Table`, `Tabs`, `Textarea`, `VTable`

`VTable` is registered with `defineAsyncComponent` by default so the VisActor chunk is loaded only when `<VTable>` is rendered.

`chip/` subfolders hold internal pieces (e.g. `Frame/chip/Menu.vue`); they are **not** registered as root components.

### `demo/` — local preview app

With **`NODE_ENV=development`**, Vite **`root`** is **`./demo`** (see `vite.config.js`) to exercise Frame, routing, and components.

| File | Role |
|------|------|
| `demo/index.html` | HTML shell |
| `demo/main.js` | Boot: `ElementPlus` + `../src/index.js` + `../src/style/index.scss` + `routes` |
| `demo/App.vue` | **`Frame`** layout, `menu: { iconClass: 'imicon', routes }`, slots `logo` / `logomini` |
| `demo/routes.js` | Sample routes: `/` → `Overview/Home`, `/a` → `A/Home`; nested `children` for the sidebar |
| `demo/views/Overview/Home.vue` | **`Page`** + **`Tabs`** switching **`FormPanel`** / **`IconPanel`** |
| `demo/views/Overview/Home/FormPanel.vue` | Form demo |
| `demo/views/Overview/Home/IconPanel.vue`, `iconfontAntNames.js` | Icon demo & Ant icon name list |
| `demo/auto-imports.d.ts` | Types from `unplugin-auto-import` (Vue / vue-router) |

Local dev:

```bash
yarn install
yarn dev
```

Dev server port **8088** (see `vite.config.js`).

### Path aliases

| Alias | Points to |
|------|-----------|
| `@` | `src/` |
| `@cps` | `src/composition/` |
| `@scss` | `assets/scss` (if present) |

Demo routes use paths like `import("/views/Overview/Home.vue")` resolved from the **`demo`** root.

---

## Using in a host application

### 1. Install peer-style deps

Align versions of **`vue`**, **`vue-router`**, **`pinia`**. For `Form` / `Table` tied to Element Plus, add **`element-plus`** and `app.use(ElementPlus)`.

### 2. Register MVFrame

```js
import { createApp } from "vue";
import mvframe from "mvframe";
// Styles: use package path when depending on source / monorepo; published `dist`-only—see “Styles”
import "mvframe/src/style/index.scss";

const app = createApp(App);

app.use(mvframe, {
  components: {
    async: ["VTable"],
  },
  vueRouter: {
    routes: yourRoutes,
    // optional: custom beforeEach, etc.
    // guard: (router) => { /* ... */ },
    // useAdmin: true,
    // adminPermission: () => true,
    // noaccess: (next) => next(false),
  },
  pinia: {
    useTab: true, // multi-tab + localStorage restore for `tabs` / `ctab`
    // storeChips: import.meta.glob("./pinia/chip/*.js", { eager: true }),
  },
  config: {
    // merged with src/config/chip/base.js → globalThis.$config
    iconfont: {
      url: "//at.alicdn.com/t/c/your_font.js",
      prefix: "ant",
    },
  },
});
```

`components.async` controls async global component registration. `VTable` is async by default to keep `@visactor/vtable` out of the initial synchronous component chain; keeping it explicit in app config documents that choice.

**Frame** injects a `<script>` for icon fonts from `globalThis.$config.iconfont.url` on mount (implementation removes `url` afterward to avoid duplicate injection).

### 3. Globals at a glance

- **`globalThis.$config`** — merged app config  
- **`globalThis.$router`** — router instance assigned by this library after install  
- **`inject("store")`** — store **factory** (`store.tab()`, `store.rmenu()`, custom chips)  
- **Global components** — use `Frame`, `Page`, `Table`, … without local registration  
- **Directives** — `v-copy`, `v-focus`  
- **Composition** — `import { ... } from "mvframe/composition"` or your `@cps` alias  

Full helper list: `.cursor/rules/util.mdc` and `src/util/index.js`.

### 4. Styles

- Utility classes & tokens: `.cursor/rules/style-system.mdc` (`--color-*`, spacing, typography, layout).  
- `vite.config.js` **`additionalData`** injects **`src/style/chip/mixin.scss`**; mirror this in apps that compile the same SCSS tree.  
- Breakpoints are centralized in `mixin.scss`: `xs / sm / md / lg / xl / xxl`. Host SCSS can `@use "mvframe/style/mixin" as *;` and use `@include media-up(md)`, `media-down(md)`, `media-only(md)`, or `media-between(sm, lg)`.  
- `mvframe/style` now ships responsive utility classes in the shape `{breakpoint}-{up|down|only}-{utility}`, for example `md-down-hide`, `lg-up-flexMode`, and `xs-only-block`.  
- Grid containers also support breakpoint column-count classes such as `class="grid col2 md2 lg3 sm1"`: default 2 columns, `sm` 1 column, `md` 2 columns, `lg` 3 columns.  
- Published packages can use **`import 'mvframe/style'`** (`dist/css/style.css`). For SCSS overrides, still link **`src/style/index.scss`** from Git/monorepo when needed.

Host SCSS example:

```scss
@use "mvframe/style/mixin" as *;

.BillingPanel {
  padding: 1.5rem;

  @include media-down(sm) {
    padding: 1rem;
  }
}
```

---

## Building the library

```bash
yarn build
```

Production uses **library mode** with entry `src/index.js` and obfuscation-related plugins (see `vite.config.js`). **`NODE_ENV=development`** points Vite at the **demo** app; to verify a real build locally, run production **`vite build`** (or your script that sets production env).

---

## DingTalk Notify Service

MVFrame ships a local Node service for DingTalk robot messages only:

```bash
yarn notify
yarn exec mvframe-notify

yarn d
yarn exec mvframe-d
```

The service reads `.env.mvframe-notify` from the host root:

```env
MVFRAME_NOTIFY_PORT=3300
MVFRAME_NOTIFY_HOST=127.0.0.1
MVFRAME_NOTIFY_TOKEN=
DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx"
DINGTALK_SECRET="SECxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

Browser code should only configure the local endpoint/token:

```js
app.use(mvframe, {
  config: {
    notify: {
      enabled: true,
      endpoint: import.meta.env.VITE_MVFRAME_NOTIFY_ENDPOINT || "http://127.0.0.1:3300",
      token: import.meta.env.VITE_MVFRAME_NOTIFY_TOKEN || "",
    },
  },
});
```

Send messages with `globalThis.$notify.send("message")` or `import { notify } from "mvframe/notify"`. Keep DingTalk webhook/secret out of browser-bundled files.

---

## Design notes

- Layout, guards, tabs, and menu behavior are **opinionated**; heavy customization may mean forking `src/router`, `src/store/tab.js`, or wrapping from the app.  
- **CSS naming & units**: root `Mvc*`, children camelCase; prefer **rem** for sizes other than 1px/2px (16px root)—see `style-system.mdc`.  
- Use **`globalThis`** or **`store` / `pinia` exports** outside `setup` (e.g. guards), as in `src/router/chip/guard.js`.

## Form Labels

`Input`, `Textarea`, `Select`, and `SelectV2` support a shared label API:

```vue
<Input v-model="form.name" label="Name" />
<Input v-model="form.name" label="Material Name" material-label />

<Textarea v-model="form.remark" label="Remark" />
<Textarea v-model="form.remark" label="Material Remark" material-label />

<Select v-model="form.type" :options="options" label="Type" />
<Select v-model="form.type" :options="options" label="Material Type" material-label />
```

Rules:

- `label` only: renders a normal top label
- `label` + `material-label`: renders a Material-style floating label
- no `label`: keeps the default control appearance

The floating label shell is implemented centrally in `src/style/chip/element.scss`. When you want a Material-style form field, prefer the built-in `label` + `material-label` API instead of recreating the same pattern locally.

---

## CLI: project skeleton

From an **empty folder or existing Vite root**, generates `src/views`, `src/component`, `src/api`, `src/assets/img`, `src/assets/style`, `src/router`, `src/pinia/chip`, `src/config`, plus starter `main.js` / `App.vue` with `app.use(mvframe, …)` (routes, Pinia `storeChips`, config, and `components.async: ["VTable"]`). It also writes/updates host-project **`AGENTS.md`** with MVFrame Codex rules, including a requirement that AI send a completion message with `yarn exec mvframe-notify --once --message "..."` after each finished development task, and copies **`/.cursor/rules/*.mdc`** from the package (same as this repo’s Cursor rules). Adds `index.html` and `vite.config.js` only if missing (use `--force` to overwrite). **Merges** `scripts` / `dependencies` / `devDependencies` into `package.json` (existing values win for shared keys), initializes `.env.mvframe-notify.example` / `.env.local.example` and `config.notify`, then runs **`yarn install`**; use `--no-package-json` to skip package changes and install. Then run **`yarn dev`** or explicitly run **`yarn exec mvframe-d`** to start Vite with the notify service.

```bash
cd /path/to/your-app
node /path/to/mvframe/scripts/scaffold-app.js
node /path/to/mvframe/scripts/scaffold-app.js --force
node /path/to/mvframe/scripts/scaffold-app.js -f

# Skip package.json (only scaffold sources + Vite config):
node /path/to/mvframe/scripts/scaffold-app.js --no-package-json
node /path/to/mvframe/scripts/scaffold-app.js -n

yarn exec mvframe-init-app
# or: npx mvframe-init-app
```

See **`MVFRAME-SCAFFOLD.md`** in the target project. Set **`MVFRAME_SCAFFOLD_TARGET`** to point at the project root.

---

## Codex Rules (`AGENTS.md`)

Codex does **not** automatically read Cursor `.cursor/rules/*.mdc`. For host projects, install the MVFrame `AGENTS.md` section so Codex prefers MVFrame global components, global helpers, and style utilities before writing local replacements:

```bash
cd /path/to/your-app
yarn exec mvframe-init-rules
# or: npx mvframe-init-rules

# Codex rules only
yarn exec mvframe-install-codex-rules
# or: npx mvframe-install-codex-rules

# Full checkout / monorepo path to mvframe
node /path/to/mvframe/scripts/init-rules.js
node /path/to/mvframe/scripts/init-rules.js /path/to/your-app

# Codex rules only
node /path/to/mvframe/scripts/install-codex-agents.js
node /path/to/mvframe/scripts/install-codex-agents.js /path/to/your-app
```

The scaffold command already runs this step. The generated block is bounded by `MVFRAME-CODEX-RULES` markers, so rerunning the command updates only that block and preserves other `AGENTS.md` content.

## Cursor Rules (`.cursor/rules`)

The unified `mvframe-init-rules` command also adds the package’s **`*.mdc`** files into the target project’s **`/.cursor/rules/`** (`component-hierarchy`, `script-setup`, `style-system`, `views`, `router`, `global-components`, `data`, `util`).
If the host project already has a same-named rule file, the host file is kept and only missing rules are added.

Install Cursor rules only:

```bash
cd /path/to/your-app
yarn exec mvframe-install-cursor-rules
# or: npx mvframe-install-cursor-rules

# Full checkout / monorepo path to mvframe
node /path/to/mvframe/scripts/install-cursor-rules.js
node /path/to/mvframe/scripts/install-cursor-rules.js /path/to/your-app
```

## Cursor Skill (app scaffold)

This repo ships **`.cursor/skills/mvframe-app-init`**. Copy it into your app’s **`.cursor/skills`** so Cursor can pick up MVFrame integration conventions.

From **your app root**, run either:

```bash
# Full checkout / monorepo path to mvframe
node /path/to/mvframe/scripts/install-cursor-skill.js
node /path/to/mvframe/scripts/install-cursor-skill.js /path/to/your-app

# After installing the npm package (the tarball must include `scripts/` and `.cursor/skills`—see `package.json` `files`)
cd /path/to/your-app
yarn exec mvframe-install-cursor-skill
# or: npx mvframe-install-cursor-skill
```

Default target is **`./.cursor/skills/mvframe-app-init`** under the current working directory. Override with **`MVFRAME_CURSOR_SKILL_OUT=/path/to/your-app`**.

---

## Changelog

- Added `mvframe-notify`, a DingTalk-only local Node notification service.
- Added `mvframe/notify` and global `$notify` for browser calls through `config.notify`.
- Added framework command `yarn d` / `mvframe-d` to run `yarn dev` with the notify service.
- `mvframe-init-app` now initializes `config.notify`, `.env.mvframe-notify` / `.env.mvframe-notify.example` with the MVFrame test DingTalk robot, and `.env.local.example` without forcing host `d` scripts.
- `VTable` now uses async global registration by default; `mvframe-init-app` writes `components.async: ["VTable"]` in `main.js`.

---

## License

ISC (see `package.json`).
