@morya-ui/setup 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,10 +4,12 @@ One-shot setup for consumer Vue projects using [`morya-ui`](https://www.npmjs.co
4
4
 
5
5
  Docs (zh/en):
6
6
 
7
- - [AI setup](https://morya-space.github.io/morya-ui/docs/ai-setup) — one-shot CLI
7
+ - [One-shot setup](https://morya-space.github.io/morya-ui/docs/setup) — CLI commands and options
8
+ - [AI setup](https://morya-space.github.io/morya-ui/docs/ai-setup) — Agent workflow
8
9
  - [Agent Skill](https://morya-space.github.io/morya-ui/docs/agent-skill) — `morya-ui-pages` behavior
10
+ - [Quick start](https://morya-space.github.io/morya-ui/docs/quick-start) — install and first component
9
11
 
10
- Installs the UI library, copies Agent skill / Cursor rules / design docs from `ai-design-config`, merges Cursor MCP for [`@morya-ui/mcp`](https://www.npmjs.com/package/@morya-ui/mcp), and tries to inject `import 'morya-ui/styles.css'` into the app entry.
12
+ Installs the UI library, copies Agent skill / Cursor rules / design docs from `design-kit`, merges Cursor MCP for [`@morya-ui/mcp`](https://www.npmjs.com/package/@morya-ui/mcp), injects `import 'morya-ui/styles.css'`, and writes `src/styles/morya-app-shell.css` with its entry import.
11
13
 
12
14
  ## Usage
13
15
 
@@ -17,6 +19,16 @@ In your app project root:
17
19
  npx @morya-ui/setup
18
20
  ```
19
21
 
22
+ Other common commands:
23
+
24
+ ```bash
25
+ # AI config + MCP only (library already installed)
26
+ npx @morya-ui/setup ai
27
+
28
+ # Library + styles only
29
+ npx @morya-ui/setup app
30
+ ```
31
+
20
32
  ### Options
21
33
 
22
34
  | Flag | Meaning |
@@ -28,10 +40,16 @@ npx @morya-ui/setup
28
40
  | `--skip-install` | Do not install `morya-ui` |
29
41
  | `--skip-template` | Do not copy AI skill / rules / docs / tokens |
30
42
  | `--skip-mcp` | Do not write `.cursor/mcp.json` |
31
- | `--skip-styles` | Do not inject `styles.css` import |
43
+ | `--skip-styles` | Do not inject `styles.css` / write app-shell CSS |
32
44
  | `--skip-scripts` | Do not add `check:colors` to `package.json` |
33
45
  | `-h`, `--help` | Show help |
34
46
 
47
+ Example: MCP only:
48
+
49
+ ```bash
50
+ npx @morya-ui/setup ai --skip-template --skip-scripts
51
+ ```
52
+
35
53
  ### Conflict policy
36
54
 
37
55
  - Template files and `.cursor/rules/*`: **skip** if the destination exists (unless `--force`).
@@ -41,7 +59,7 @@ npx @morya-ui/setup
41
59
 
42
60
  ### What gets copied
43
61
 
44
- From the package `template/` (synced from repo `ai-design-config/`):
62
+ From the package `template/` (synced from repo `design-kit/`):
45
63
 
46
64
  - `DESIGN.md`
47
65
  - `.agents/skills/morya-ui-pages/`
@@ -51,6 +69,8 @@ From the package `template/` (synced from repo `ai-design-config/`):
51
69
  - `scripts/check-raw-colors.mjs`
52
70
  - `src/examples/`
53
71
 
72
+ `morya-app-shell.css` is written by the CLI (not copied from the template tree).
73
+
54
74
  ### MCP (Cursor)
55
75
 
56
76
  Writes / merges:
@@ -76,6 +96,7 @@ Looks for `src/main.ts` / `.js` / `.tsx` / `.jsx`, root `main.*`, `src/app.ts` /
76
96
 
77
97
  ```ts
78
98
  import 'morya-ui/styles.css'
99
+ import './styles/morya-app-shell.css'
79
100
  ```
80
101
 
81
102
  The CLI does **not** call `app.use(MoryaUI)` or edit `App.vue`.
@@ -83,7 +104,7 @@ The CLI does **not** call `app.use(MoryaUI)` or edit `App.vue`.
83
104
  ## Local development (this monorepo)
84
105
 
85
106
  ```bash
86
- pnpm setup:sync-template # refresh packages/setup/template from ai-design-config
107
+ pnpm setup:sync-template # refresh packages/setup/template from design-kit
87
108
  pnpm setup:build
88
109
  node packages/setup/bin/morya-ui-setup.js --cwd /path/to/app --dry-run
89
110
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morya-ui/setup",
3
3
  "type": "module",
4
- "version": "0.2.5",
4
+ "version": "0.2.7",
5
5
  "description": "One-shot setup for morya-ui: install the library, Cursor MCP, Agent skills, and design config.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/morya-space/morya-ui/tree/main/packages/setup#readme",
package/src/cli.mjs CHANGED
@@ -1,15 +1,37 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { dirname, join, resolve } from 'node:path'
2
3
  import { fileURLToPath } from 'node:url'
3
4
  import { copyTemplate } from './copy-template.mjs'
5
+ import { readJson } from './fs-utils.mjs'
4
6
  import { installMoryaUi } from './install.mjs'
5
7
  import { mergeMcpConfig } from './mcp.mjs'
6
8
  import { ensureCheckColorsScript } from './package-json.mjs'
7
- import { ensureStylesImport } from './styles.mjs'
9
+ import { ensureShellStyles, ensureStylesImport } from './styles.mjs'
8
10
 
9
11
  const __dirname = dirname(fileURLToPath(import.meta.url))
10
12
  const PKG_ROOT = resolve(__dirname, '..')
11
13
  const TEMPLATE_ROOT = join(PKG_ROOT, 'template')
12
14
 
15
+ const MODES = new Set(['app', 'ai', 'full'])
16
+
17
+ /** AI pack paths under template/ (excludes runtime src/styles — owned by app mode). */
18
+ const AI_TEMPLATE_INCLUDE = [
19
+ 'DESIGN.md',
20
+ '.agents/skills/morya-ui-pages',
21
+ '.cursor/rules',
22
+ 'docs',
23
+ 'design-tokens',
24
+ 'scripts/check-raw-colors.mjs',
25
+ 'src/examples',
26
+ ]
27
+
28
+ /** Mode → default skips (user --skip-* can only add more skips). */
29
+ const MODE_DEFAULTS = {
30
+ full: {},
31
+ app: { skipTemplate: true, skipMcp: true, skipScripts: true },
32
+ ai: { skipInstall: true, skipStyles: true },
33
+ }
34
+
13
35
  const SKIP_FLAGS = {
14
36
  '--skip-install': 'skipInstall',
15
37
  '--skip-template': 'skipTemplate',
@@ -19,13 +41,20 @@ const SKIP_FLAGS = {
19
41
  }
20
42
 
21
43
  export function printHelp() {
22
- console.log(`Usage: morya-ui-setup [options]
44
+ console.log(`Usage: morya-ui-setup [command] [options]
23
45
 
24
- One-shot setup for morya-ui in a consumer Vue project:
46
+ Commands:
47
+ (default) / full Install morya-ui, AI template, MCP, styles, check:colors
48
+ app Install morya-ui and inject styles / app-shell CSS
49
+ ai Copy Agent skill / rules / DESIGN / docs / tokens / examples,
50
+ merge Cursor MCP, add check:colors
51
+
52
+ Default command:
25
53
  - install morya-ui
26
54
  - copy DESIGN.md, Agent skill, Cursor rules, docs, tokens, examples
27
55
  - merge .cursor/mcp.json for @morya-ui/mcp
28
56
  - inject import 'morya-ui/styles.css' into the app entry when found
57
+ - write src/styles/morya-app-shell.css and inject its import
29
58
  - add check:colors script when missing
30
59
 
31
60
  Options:
@@ -36,7 +65,7 @@ Options:
36
65
  --skip-install Skip dependency install
37
66
  --skip-template Skip copying AI template files
38
67
  --skip-mcp Skip writing .cursor/mcp.json
39
- --skip-styles Skip injecting styles.css import
68
+ --skip-styles Skip injecting styles.css / app-shell CSS
40
69
  --skip-scripts Skip adding check:colors to package.json
41
70
  -h, --help Show this help
42
71
  `)
@@ -47,6 +76,7 @@ Options:
47
76
  */
48
77
  export function parseArgs(argv) {
49
78
  const options = {
79
+ mode: 'full',
50
80
  cwd: process.cwd(),
51
81
  pm: undefined,
52
82
  force: false,
@@ -65,6 +95,10 @@ export function parseArgs(argv) {
65
95
  options.help = true
66
96
  continue
67
97
  }
98
+ if (MODES.has(arg)) {
99
+ options.mode = arg
100
+ continue
101
+ }
68
102
  if (arg === '--force') {
69
103
  options.force = true
70
104
  continue
@@ -100,6 +134,11 @@ export function parseArgs(argv) {
100
134
  throw new Error(`Unknown argument: ${arg}`)
101
135
  }
102
136
 
137
+ const defaults = MODE_DEFAULTS[options.mode] || {}
138
+ for (const [key, value] of Object.entries(defaults)) {
139
+ if (value) options[key] = true
140
+ }
141
+
103
142
  return options
104
143
  }
105
144
 
@@ -108,11 +147,30 @@ function rel(cwd, path) {
108
147
  return path.startsWith(cwd) ? path.slice(cwd.length).replace(/^[\\/]/, '') || path : path
109
148
  }
110
149
 
150
+ /**
151
+ * @param {string} cwd
152
+ */
153
+ function hasMoryaUiDependency(cwd) {
154
+ const path = join(cwd, 'package.json')
155
+ if (!existsSync(path)) return false
156
+ try {
157
+ const pkg = readJson(path)
158
+ return Boolean(
159
+ pkg.dependencies?.['morya-ui']
160
+ || pkg.devDependencies?.['morya-ui']
161
+ || pkg.peerDependencies?.['morya-ui'],
162
+ )
163
+ } catch {
164
+ return false
165
+ }
166
+ }
167
+
111
168
  /**
112
169
  * @param {ReturnType<typeof parseArgs>} options
113
170
  */
114
171
  export async function runSetup(options) {
115
172
  const {
173
+ mode,
116
174
  cwd,
117
175
  force,
118
176
  dryRun,
@@ -124,14 +182,25 @@ export async function runSetup(options) {
124
182
  pm,
125
183
  } = options
126
184
 
127
- console.log(`@morya-ui/setup → ${cwd}${dryRun ? ' (dry-run)' : ''}`)
185
+ console.log(`@morya-ui/setup [${mode}] → ${cwd}${dryRun ? ' (dry-run)' : ''}`)
128
186
  console.log('')
129
187
 
188
+ if (mode === 'ai' && !hasMoryaUiDependency(cwd)) {
189
+ console.log(
190
+ 'Warning: morya-ui is not listed in package.json. Run `npx @morya-ui/setup app` first (or pnpm add morya-ui).',
191
+ )
192
+ console.log('')
193
+ }
194
+
130
195
  const install = installMoryaUi(cwd, { pm, dryRun, skipInstall })
131
196
 
132
197
  const template = skipTemplate
133
198
  ? { copied: [], skipped: [], forced: [], skippedStep: true }
134
- : copyTemplate(TEMPLATE_ROOT, cwd, { force, dryRun })
199
+ : copyTemplate(TEMPLATE_ROOT, cwd, {
200
+ force,
201
+ dryRun,
202
+ include: mode === 'ai' || mode === 'full' ? AI_TEMPLATE_INCLUDE : undefined,
203
+ })
135
204
 
136
205
  const mcp = skipMcp
137
206
  ? { path: join(cwd, '.cursor', 'mcp.json'), action: 'skipped-flag' }
@@ -141,6 +210,10 @@ export async function runSetup(options) {
141
210
  ? { action: 'skipped', reason: 'skip-styles' }
142
211
  : ensureStylesImport(cwd, { dryRun })
143
212
 
213
+ const shell = skipStyles
214
+ ? { fileAction: 'skipped', importAction: 'skipped', reason: 'skip-styles' }
215
+ : ensureShellStyles(cwd, { dryRun, force })
216
+
144
217
  const scripts = skipScripts
145
218
  ? { action: 'skipped-flag' }
146
219
  : ensureCheckColorsScript(cwd, { force, dryRun })
@@ -153,7 +226,7 @@ export async function runSetup(options) {
153
226
  }
154
227
 
155
228
  if (template.skippedStep) {
156
- console.log('Template: skipped (--skip-template)')
229
+ console.log('Template: skipped (--skip-template or app mode)')
157
230
  } else {
158
231
  console.log(
159
232
  `Template: ${template.copied.length} copied, ${template.forced.length} overwritten, ${template.skipped.length} skipped`,
@@ -167,13 +240,13 @@ export async function runSetup(options) {
167
240
  }
168
241
 
169
242
  if (mcp.action === 'skipped-flag') {
170
- console.log('MCP: skipped (--skip-mcp)')
243
+ console.log('MCP: skipped (--skip-mcp or app mode)')
171
244
  } else {
172
245
  console.log(`MCP: ${mcp.action} (${rel(cwd, mcp.path) || '.cursor/mcp.json'})`)
173
246
  }
174
247
 
175
248
  if (styles.reason === 'skip-styles') {
176
- console.log('Styles: skipped (--skip-styles)')
249
+ console.log('Styles: skipped (--skip-styles or ai mode)')
177
250
  } else if (styles.action === 'injected') {
178
251
  console.log(`Styles: injected into ${rel(cwd, styles.path)}`)
179
252
  } else if (styles.action === 'skipped') {
@@ -183,18 +256,38 @@ export async function runSetup(options) {
183
256
  console.log(" import 'morya-ui/styles.css'")
184
257
  }
185
258
 
259
+ if (shell.reason === 'skip-styles') {
260
+ console.log('Shell CSS: skipped (--skip-styles or ai mode)')
261
+ } else {
262
+ console.log(
263
+ `Shell CSS: file ${shell.fileAction}`
264
+ + (shell.path ? ` (${rel(cwd, shell.path)})` : '')
265
+ + `, import ${shell.importAction}`
266
+ + (shell.entry ? ` (${rel(cwd, shell.entry)})` : ''),
267
+ )
268
+ }
269
+
186
270
  if (scripts.action === 'skipped-flag') {
187
- console.log('Scripts: skipped (--skip-scripts)')
271
+ console.log('Scripts: skipped (--skip-scripts or app mode)')
188
272
  } else {
189
273
  console.log(`Scripts: check:colors ${scripts.action}`)
190
274
  }
191
275
 
192
276
  console.log('')
193
277
  console.log('Next:')
194
- console.log(' 1. Ensure the styles import is in your app entry (if not injected).')
195
- console.log(' 2. Restart Cursor (or reload MCP) so morya-ui MCP tools appear.')
196
- console.log(' 3. Have the agent read DESIGN.md before generating pages.')
197
- console.log(' 4. Optional: pnpm check:colors')
278
+ if (mode === 'app') {
279
+ console.log(' 1. Ensure styles.css + morya-app-shell.css imports are in your app entry.')
280
+ console.log(' 2. Optional AI pack: npx @morya-ui/setup ai')
281
+ } else if (mode === 'ai') {
282
+ console.log(' 1. Restart Cursor (or reload MCP) so morya-ui MCP tools appear.')
283
+ console.log(' 2. Have the agent read DESIGN.md before generating pages.')
284
+ console.log(' 3. Optional: pnpm check:colors')
285
+ } else {
286
+ console.log(' 1. Ensure styles.css + morya-app-shell.css imports are in your app entry.')
287
+ console.log(' 2. Restart Cursor (or reload MCP) so morya-ui MCP tools appear.')
288
+ console.log(' 3. Have the agent read DESIGN.md before generating pages.')
289
+ console.log(' 4. Optional: pnpm check:colors')
290
+ }
198
291
 
199
- return { install, template, mcp, styles, scripts }
292
+ return { mode, install, template, mcp, styles, shell, scripts }
200
293
  }
@@ -20,11 +20,27 @@ function listFiles(dir) {
20
20
  return out
21
21
  }
22
22
 
23
+ /**
24
+ * @param {string} rel
25
+ * @param {string[] | undefined} include
26
+ */
27
+ function matchesInclude(rel, include) {
28
+ if (!include || include.length === 0) return true
29
+ const norm = rel.replace(/\\/g, '/')
30
+ return include.some((prefix) => {
31
+ const p = prefix.replace(/\\/g, '/')
32
+ return norm === p || norm.startsWith(`${p}/`)
33
+ })
34
+ }
35
+
23
36
  /**
24
37
  * Copy template tree into target cwd.
38
+ * @param {string} templateRoot
39
+ * @param {string} cwd
40
+ * @param {{ force?: boolean, dryRun?: boolean, include?: string[] }} [options]
25
41
  * @returns {{ copied: string[], skipped: string[], forced: string[] }}
26
42
  */
27
- export function copyTemplate(templateRoot, cwd, { force = false, dryRun = false } = {}) {
43
+ export function copyTemplate(templateRoot, cwd, { force = false, dryRun = false, include } = {}) {
28
44
  const copied = []
29
45
  const skipped = []
30
46
  const forced = []
@@ -35,7 +51,7 @@ export function copyTemplate(templateRoot, cwd, { force = false, dryRun = false
35
51
  )
36
52
  }
37
53
 
38
- const files = listFiles(templateRoot)
54
+ const files = listFiles(templateRoot).filter((rel) => matchesInclude(rel, include))
39
55
  for (const rel of files) {
40
56
  const from = join(templateRoot, rel)
41
57
  const to = join(cwd, rel)
package/src/fs-utils.mjs CHANGED
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import { dirname } from 'node:path'
3
3
 
4
4
  export function readJson(path) {
5
- return JSON.parse(readFileSync(path, 'utf8'))
5
+ const text = readFileSync(path, 'utf8').replace(/^\uFEFF/, '')
6
+ return JSON.parse(text)
6
7
  }
7
8
 
8
9
  export function writeJson(path, value, { dryRun = false } = {}) {
package/src/styles.mjs CHANGED
@@ -1,8 +1,10 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
- import { join, resolve } from 'node:path'
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join, relative, resolve } from 'node:path'
3
3
 
4
4
  const STYLE_IMPORT = "import 'morya-ui/styles.css'"
5
5
  const STYLE_MARKER = 'morya-ui/styles.css'
6
+ const SHELL_MARKER = 'morya-app-shell.css'
7
+ const SHELL_REL = 'src/styles/morya-app-shell.css'
6
8
 
7
9
  const CANDIDATES = [
8
10
  'src/main.ts',
@@ -15,6 +17,14 @@ const CANDIDATES = [
15
17
  'src/app.js',
16
18
  ]
17
19
 
20
+ const SHELL_CSS = `html,
21
+ body,
22
+ #app {
23
+ height: 100%;
24
+ margin: 0;
25
+ }
26
+ `
27
+
18
28
  /**
19
29
  * @param {string} cwd
20
30
  * @returns {string | null} absolute path
@@ -41,11 +51,13 @@ export function findEntryFile(cwd) {
41
51
  }
42
52
 
43
53
  /**
44
- * Insert style import after the last leading import, or at top.
54
+ * Insert an import after the last leading import, or at top.
45
55
  * @param {string} source
56
+ * @param {string} importLine
57
+ * @param {string} marker substring that means "already present"
46
58
  */
47
- export function injectStyleImport(source) {
48
- if (source.includes(STYLE_MARKER)) {
59
+ export function injectImportLine(source, importLine, marker) {
60
+ if (source.includes(marker)) {
49
61
  return { source, changed: false, reason: 'already-present' }
50
62
  }
51
63
 
@@ -69,17 +81,35 @@ export function injectStyleImport(source) {
69
81
  }
70
82
 
71
83
  if (lastImportIndex >= 0) {
72
- lines.splice(lastImportIndex + 1, 0, STYLE_IMPORT)
73
- } else {
74
- // After shebang / 'use strict' if present
84
+ lines.splice(lastImportIndex + 1, 0, importLine)
85
+ }
86
+ else {
75
87
  let insertAt = 0
76
88
  if (lines[0]?.startsWith('#!')) insertAt = 1
77
- lines.splice(insertAt, 0, STYLE_IMPORT, '')
89
+ lines.splice(insertAt, 0, importLine, '')
78
90
  }
79
91
 
80
92
  return { source: lines.join('\n'), changed: true }
81
93
  }
82
94
 
95
+ /**
96
+ * @param {string} source
97
+ * @deprecated use injectImportLine
98
+ */
99
+ export function injectStyleImport(source) {
100
+ return injectImportLine(source, STYLE_IMPORT, STYLE_MARKER)
101
+ }
102
+
103
+ /**
104
+ * @param {string} entryFile absolute
105
+ * @param {string} shellFile absolute
106
+ */
107
+ export function shellImportLine(entryFile, shellFile) {
108
+ let rel = relative(dirname(entryFile), shellFile).replace(/\\/g, '/')
109
+ if (!rel.startsWith('.')) rel = `./${rel}`
110
+ return `import '${rel}'`
111
+ }
112
+
83
113
  /**
84
114
  * @returns {{
85
115
  * action: 'injected' | 'skipped' | 'missing-entry',
@@ -95,7 +125,7 @@ export function ensureStylesImport(cwd, { dryRun = false } = {}) {
95
125
  }
96
126
 
97
127
  const original = readFileSync(entry, 'utf8')
98
- const { source, changed, reason } = injectStyleImport(original)
128
+ const { source, changed, reason } = injectImportLine(original, STYLE_IMPORT, STYLE_MARKER)
99
129
 
100
130
  if (!changed) {
101
131
  return { action: 'skipped', path: entry, reason: reason || 'already-present' }
@@ -107,3 +137,68 @@ export function ensureStylesImport(cwd, { dryRun = false } = {}) {
107
137
 
108
138
  return { action: 'injected', path: entry, dryRun }
109
139
  }
140
+
141
+ /**
142
+ * Write morya-app-shell.css (height chain) and inject its import into the entry.
143
+ * @returns {{
144
+ * fileAction: 'written' | 'skipped' | 'dry-run',
145
+ * importAction: 'injected' | 'skipped' | 'missing-entry',
146
+ * path?: string,
147
+ * entry?: string,
148
+ * reason?: string,
149
+ * dryRun?: boolean
150
+ * }}
151
+ */
152
+ export function ensureShellStyles(cwd, { dryRun = false, force = false } = {}) {
153
+ const shellFile = join(cwd, SHELL_REL)
154
+ const entry = findEntryFile(cwd)
155
+
156
+ let fileAction = 'skipped'
157
+ if (!existsSync(shellFile) || force) {
158
+ if (dryRun) {
159
+ fileAction = 'dry-run'
160
+ }
161
+ else {
162
+ mkdirSync(dirname(shellFile), { recursive: true })
163
+ writeFileSync(shellFile, SHELL_CSS, 'utf8')
164
+ fileAction = 'written'
165
+ }
166
+ }
167
+
168
+ if (!entry) {
169
+ return {
170
+ fileAction,
171
+ importAction: 'missing-entry',
172
+ path: shellFile,
173
+ reason: 'no-entry-found',
174
+ dryRun,
175
+ }
176
+ }
177
+
178
+ const importLine = shellImportLine(entry, shellFile)
179
+ const original = readFileSync(entry, 'utf8')
180
+ const { source, changed, reason } = injectImportLine(original, importLine, SHELL_MARKER)
181
+
182
+ if (!changed) {
183
+ return {
184
+ fileAction,
185
+ importAction: 'skipped',
186
+ path: shellFile,
187
+ entry,
188
+ reason: reason || 'already-present',
189
+ dryRun,
190
+ }
191
+ }
192
+
193
+ if (!dryRun) {
194
+ writeFileSync(entry, source, 'utf8')
195
+ }
196
+
197
+ return {
198
+ fileAction,
199
+ importAction: 'injected',
200
+ path: shellFile,
201
+ entry,
202
+ dryRun,
203
+ }
204
+ }
@@ -47,8 +47,8 @@ Full API: docs site `/components` or MCP (`get_component`, `search`, `validate_u
47
47
  | Intent | Prefer |
48
48
  | --- | --- |
49
49
  | Searchable list + paging | `MPageFilters` + `MTable` (+ paginator) |
50
- | Create / edit entity page | Form golden layout + `MForm` |
51
- | Create / edit in place | `MDialog` or `MDrawer` + form |
50
+ | Create / edit from a list (few–medium fields) | **`MDialog` + `MForm`** (stay on the list) |
51
+ | Create / edit long / multi-section entity | Form golden page **or** `MDrawer` |
52
52
  | Delete | `MConfirmDialog` |
53
53
  | Lightweight inline status | `MStatus` (dot + label) |
54
54
  | Status chip / closable label | `MTag` severities |
@@ -60,6 +60,7 @@ Full API: docs site `/components` or MCP (`get_component`, `search`, `validate_u
60
60
  | Empty list / zero state | `MEmpty` (+ `empty-state` golden or `MTable` `#empty`) |
61
61
  | Submit success / HTTP error page | `MResult` |
62
62
  | Local capped scroll | Explicit `MScrollbar` |
63
+ | Admin sider nav | `MMenu` |
63
64
 
64
65
  ## Common mistakes
65
66
 
@@ -21,7 +21,14 @@ import 'morya-ui/styles.css'
21
21
  <template>
22
22
  <MConfigProvider :locale="zhCN">
23
23
  <MLayout fill-viewport has-sider>
24
- <MLayoutSider bordered>...</MLayoutSider>
24
+ <MLayoutSider bordered>
25
+ <MMenu
26
+ :model="[
27
+ { key: 'users', label: 'Users', icon: 'user', to: '/users' },
28
+ { key: 'roles', label: 'Roles', icon: 'shield', to: '/roles' },
29
+ ]"
30
+ />
31
+ </MLayoutSider>
25
32
  <MLayout>
26
33
  <MLayoutHeader>...</MLayoutHeader>
27
34
  <MLayoutContent>...</MLayoutContent>
@@ -34,6 +41,7 @@ import 'morya-ui/styles.css'
34
41
  | Role | Prefer |
35
42
  | --- | --- |
36
43
  | Admin chrome | `MLayout fillViewport` + sider / header / content |
44
+ | Sider nav | `MMenu` |
37
45
  | Page stack | `MPageContent` inside `MLayoutContent` |
38
46
  | Filters / toolbar | `MPageFilters` + `MPageToolbar` |
39
47
  | Form surfaces | `MPageHeader` + `MPageSection variant="form|actions"` |
@@ -16,12 +16,14 @@ Via MCP: `recommend_page` → `get_golden_page`; local edits: `get_page_snippet`
16
16
  ## List page — block order
17
17
 
18
18
  1. `MLayout fillViewport` + optional `MLayoutSider bordered`
19
- 2. `MLayoutHeader` → `MBreadcrumb`
20
- 3. `MLayoutContent` → `MPageContent`
21
- 4. `MPageFilters` — inner `MSpace` + Input/Select + query/reset
22
- 5. `MPageToolbar` — title + `#actions` primary action
23
- 6. `MTable` directly in content (usually **no** wrapping `MCard`)
24
- 7. Pagination via `MTable` paginator or sibling `MPagination`
19
+ 2. Sider `MMenu`
20
+ 3. `MLayoutHeader` → `MBreadcrumb`
21
+ 4. `MLayoutContent` → `MPageContent`
22
+ 5. `MPageFilters` — inner `MSpace` + Input/Select + query/reset
23
+ 6. `MPageToolbar` — title + `#actions` primary action
24
+ 7. `MTable` directly in content (usually **no** wrapping `MCard`)
25
+ 8. Pagination via `MTable` paginator or sibling `MPagination`
26
+ 9. Short create/edit → `MDialog` + `MForm` on the same page (default)
25
27
 
26
28
  ## Form page — block order
27
29
 
@@ -42,7 +44,7 @@ Via MCP: `recommend_page` → `get_golden_page`; local edits: `get_page_snippet`
42
44
 
43
45
  | Topic | Prefer | Usually avoid |
44
46
  | --- | --- | --- |
45
- | Shell | `MLayout fillViewport` + `MPageContent` | Hand `min-height: 100vh`; padding on `MLayoutContent` |
47
+ | Shell | `MLayout fillViewport` + `MPageContent` | Padding on `MLayoutContent` |
46
48
  | Sections | `MPageFilters` / `MPageToolbar` / `MPageSection` | Custom `.page-*`; extra `MCard` wrappers |
47
49
  | List table | `MTable` in `MPageContent` | Border card solely to wrap the table |
48
50
  | Spacing | `MSpace` / `MFlex` for peers; page gap from `MPageContent` | Nested padded divs stacking gaps |
@@ -6,12 +6,12 @@ Pick a lane, then a surface. Prefer project golden pages for **Ops**; use the re
6
6
 
7
7
  | Surface | Job | Compose from |
8
8
  | --- | --- | --- |
9
- | List | Scan, filter, act on many rows | Golden list + [page-layouts.md](page-layouts.md) |
10
- | Form | Create / edit one entity | Golden form |
9
+ | List | Scan, filter, act on many rows | Golden list + [page-layouts.md](page-layouts.md); **create/edit with ≤~8 fields → `MDialog` on the list** |
10
+ | Form | Create / edit one entity as a **dedicated page** | Golden form — only when multi-section, many fields, or user asked for an independent form route |
11
11
  | Dashboard | Orient + jump to work | Golden dashboard |
12
- | Detail | Read-heavy record + secondary actions | `MPageHeader` + `MDescriptions`-style sections via `MCard`/`MPageSection`, actions in toolbar; edit via route or `MDrawer`/`MDialog` |
12
+ | Detail | Read-heavy record + secondary actions | `MPageHeader` + sections via `MCard`/`MPageSection`; edit via `MDialog` / `MDrawer` (prefer over a second form route) |
13
13
  | Settings | Grouped preferences | Narrow `MPageContent` + stacked `MPageSection` / `MTabs` + `MForm` |
14
- | Hybrid list | List + drawer/dialog | List golden + overlay recipe |
14
+ | Hybrid list | List + drawer/dialog | **Default Ops CRUD pattern**: list golden + `MDialog`/`MDrawer` form |
15
15
 
16
16
  ## Account
17
17
 
@@ -10,21 +10,27 @@ alwaysApply: false
10
10
  | 类型 | 模板 |
11
11
  | --- | --- |
12
12
  | 列表页 | `docs/golden-pages/list-page.vue` |
13
- | 表单页 | `docs/golden-pages/form-page.vue` |
13
+ | 表单页 | `docs/golden-pages/form-page.vue`(仅长表单 / 独立配置页) |
14
14
  | 仪表盘 | `docs/golden-pages/dashboard-page.vue` |
15
15
  | 登录页 | `docs/golden-pages/login-page.vue` |
16
16
  | 营销落地 | `docs/golden-pages/landing-page.vue` |
17
17
  | 空状态 | `docs/golden-pages/empty-state.vue` |
18
18
 
19
+ ## 列表内新建 / 编辑(默认)
20
+
21
+ 后台 CRUD **优先**:列表页 + `MDialog`(或 `MDrawer`)内 `MForm`,不要把「新建用户 / 编辑角色」做成单独路由,除非字段很多或多步骤。
22
+
19
23
  ## 列表页区块顺序
20
24
 
21
25
  1. `MLayout fillViewport` + 可选 `MLayoutSider bordered`
22
- 2. `MLayoutHeader` → `MBreadcrumb`
23
- 3. `MLayoutContent` → `MPageContent`
24
- 4. `MPageFilters`(内 `MSpace` + Input/Select + 查询/重置)
25
- 5. `MPageToolbar`(标题 + `#actions` 主操作)
26
- 6. `MTable`(直接放置,通常不必包 `MCard`)
27
- 7. 分页由 `MTable paginator` 或同级 `MPagination`
26
+ 2. 侧栏 `MMenu`
27
+ 3. `MLayoutHeader` → `MBreadcrumb`
28
+ 4. `MLayoutContent` → `MPageContent`
29
+ 5. `MPageFilters`(内 `MSpace` + Input/Select + 查询/重置)
30
+ 6. `MPageToolbar`(标题 + `#actions` 主操作)
31
+ 7. `MTable`(直接放置,通常不必包 `MCard`)
32
+ 8. 分页由 `MTable paginator` 或同级 `MPagination`
33
+ 9. 短新建/编辑:同页 `MDialog` + `MForm`
28
34
 
29
35
  ## 表单页区块顺序
30
36
 
@@ -47,7 +53,7 @@ alwaysApply: false
47
53
 
48
54
  | 主题 | 推荐 | 通常不必 |
49
55
  | --- | --- | --- |
50
- | 骨架 | `MLayout fillViewport` + `MPageContent` | 手写 `min-height:100vh`;`MLayoutContent` 再写 padding |
56
+ | 骨架 | `MLayout fillViewport` + `MPageContent` | `MLayoutContent` 再写 padding |
51
57
  | 区块 | `MPageFilters` / `MPageToolbar` / `MPageSection` | 手写 `.page-*`;`MCard` 再包一层 |
52
58
  | 列表表格 | `MTable` 直接放在 `MPageContent` | 用 `MCard` 再包 bordered 表格 |
53
59
  | 控件间距 | 同行用 `MSpace` / `MFlex`;区块靠 `MPageContent` gap | 外包 padded `div` 叠间距 |
@@ -1,6 +1,6 @@
1
1
  # Morya UI 设计系统(AI 第一信源)
2
2
 
3
- > 基于 `morya-ui` v0.1.x。生成或审查业务页面时,**必须先遵守本文**,再查阅 `docs/components.md` 与 `docs/golden-pages/`。
3
+ > 基于 `morya-ui` v0.2.6。生成或审查业务页面时,**必须先遵守本文**,再查阅 `docs/components.md` 与 `docs/golden-pages/`。
4
4
 
5
5
  ## 1. 设计原则
6
6
 
@@ -20,8 +20,15 @@ import 'morya-ui/styles.css'
20
20
 
21
21
  <template>
22
22
  <MConfigProvider :locale="zhCN">
23
- <MLayout has-sider>
24
- <MLayoutSider>...</MLayoutSider>
23
+ <MLayout has-sider fill-viewport>
24
+ <MLayoutSider bordered>
25
+ <MMenu
26
+ :model="[
27
+ { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
28
+ { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
29
+ ]"
30
+ />
31
+ </MLayoutSider>
25
32
  <MLayout>
26
33
  <MLayoutHeader>...</MLayoutHeader>
27
34
  <MLayoutContent>...</MLayoutContent>
@@ -32,6 +39,7 @@ import 'morya-ui/styles.css'
32
39
  ```
33
40
 
34
41
  - 管理后台:`MLayout fillViewport` + `MLayoutSider` + `MLayoutHeader` + `MLayoutContent`
42
+ - 侧栏导航:`MMenu`
35
43
  - 页面内容堆叠:`MPageContent`(内置于 `MLayoutContent`)
36
44
  - 列表筛选 / 工具栏:`MPageFilters` + `MPageToolbar`
37
45
  - 表单引导 / 表面:`MPageHeader` + `MPageSection variant="form|actions"`
@@ -56,9 +64,11 @@ import 'morya-ui/styles.css'
56
64
  ## 4. 表单约定
57
65
 
58
66
  - 使用 `MForm` + `MFormItem`,`name` 与校验规则对应。
67
+ - **从列表新建/编辑(字段约 ≤8、单段表单)默认用 `MDialog` + `MForm`,不要为每个实体再开独立路由表单页。**
68
+ - 多分组、长流程、向导式录入再用独立表单页(`docs/golden-pages/form-page.vue`)或 `MDrawer`。
59
69
  - 字段组件自带 `label` / `invalid` / `helpText` 时优先用组件 prop(如 `MInput`、`MSelect`),复杂表单再用 `MFormItem` 包一层。
60
70
  - 尺寸:默认 medium;密集后台可 `size="small"` 或 ConfigProvider `globalDensity`。
61
- - 宽度:筛选项 `fluid` 慎用;表单页主栏 `max-width: 40rem` 左右。
71
+ - 宽度:筛选项 `fluid` 慎用;独立表单页主栏 `max-width: 40rem` 左右。
62
72
 
63
73
  ## 5. 数据展示
64
74
 
@@ -98,7 +108,7 @@ import 'morya-ui/styles.css'
98
108
 
99
109
  ## 8. 禁止项
100
110
 
101
- - 禁止引入第二套 UI 库(Element Plus、Naive UI 等)混用同一页面。
111
+ - 禁止引入第二套 UI 库混用同一页面。
102
112
  - 禁止在业务 CSS 中写死主题色;暗色模式必须能随 `[data-theme="dark"]` 生效。
103
113
  - 禁止用 `<div onclick>` 代替 `<button>` / `MButton`。
104
114
  - 禁止 Select 与 Dropdown 混用:选项选择用 `MSelect` / `MTreeSelect`;动作菜单用 `MDropdown`。
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://design-tokens.github.io/community-group/format/",
3
3
  "meta": {
4
4
  "name": "morya-ui",
5
- "version": "0.1.1",
5
+ "version": "0.2.6",
6
6
  "source": "morya-ui/src/theme/styles.css",
7
7
  "prefix": "m"
8
8
  },
@@ -114,7 +114,8 @@
114
114
  | 我要做… | 首选组件 |
115
115
  | --- | --- |
116
116
  | 用户列表 + 搜索 + 分页 | `MTable` + `MInput` + `MPagination` |
117
- | 新建/编辑实体 | `MForm` + 字段组件 + `MDialog` 或独立路由页 |
117
+ | 新建/编辑实体(短表单,列表内) | **`MDialog` + `MForm` + 字段组件**(首选) |
118
+ | 新建/编辑实体(长表单 / 多分组) | `MForm` + 独立表单页或 `MDrawer` |
118
119
  | 删除确认 | `MConfirmDialog` |
119
120
  | 筛选侧栏 | `MDrawer` + 表单控件 |
120
121
  | 状态指示(行内) | `MStatus`(圆点 + 文案) |
@@ -88,13 +88,13 @@ message.error('操作失败')
88
88
  | 删除确认用 Message/Toast | `MConfirmDialog` |
89
89
  | 所有反馈都用 Toast | 默认改 Message,仅 detail 场景保留 Toast |
90
90
 
91
- ## 与 Naive / Element Plus 的对应
91
+ ## 概念对照
92
92
 
93
- | Morya UI | 近似概念 |
93
+ | API | 用途 |
94
94
  | --- | --- |
95
- | `message` | Naive `message` / Element `ElMessage` |
96
- | `toast` | Naive `notification` / Element `ElNotification` |
97
- | `<MMessage>` | 页面内 `el-alert` / 表单顶部错误条 |
95
+ | `message` | 轻量、短暂的操作反馈(默认首选) |
96
+ | `toast` | 带摘要 + 详情、或需停留更久的通知 |
97
+ | `<MMessage>` | 页面内常驻提示条(如表单顶部错误) |
98
98
 
99
99
  ## 相关文档
100
100
 
@@ -26,6 +26,12 @@ import { ref } from 'vue'
26
26
 
27
27
  const keyword = ref('')
28
28
  const status = ref<string | undefined>()
29
+ const siderCollapsed = ref(false)
30
+
31
+ const menuModel = [
32
+ { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
33
+ { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
34
+ ]
29
35
 
30
36
  const statusOptions = [
31
37
  { label: '全部', value: '' },
@@ -52,8 +58,13 @@ const rows = [
52
58
  <template>
53
59
  <MConfigProvider :locale="zhCN">
54
60
  <MLayout has-sider fill-viewport>
55
- <MLayoutSider bordered>
56
- <MMenu :model="[{ label: '用户管理', key: 'users' }, { label: '角色管理', key: 'roles' }]" />
61
+ <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
62
+ <MMenu
63
+ :model="menuModel"
64
+ :collapsed="siderCollapsed"
65
+ :collapsed-width="72"
66
+ selected-key="users"
67
+ />
57
68
  </MLayoutSider>
58
69
 
59
70
  <MLayout>
@@ -26,6 +26,12 @@ import { ref } from 'vue'
26
26
 
27
27
  const keyword = ref('')
28
28
  const status = ref<string | undefined>()
29
+ const siderCollapsed = ref(false)
30
+
31
+ const menuModel = [
32
+ { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
33
+ { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
34
+ ]
29
35
 
30
36
  const statusOptions = [
31
37
  { label: '全部', value: '' },
@@ -52,8 +58,13 @@ const rows = [
52
58
  <template>
53
59
  <MConfigProvider :locale="zhCN">
54
60
  <MLayout has-sider fill-viewport>
55
- <MLayoutSider bordered>
56
- <MMenu :model="[{ label: '用户管理', key: 'users' }, { label: '角色管理', key: 'roles' }]" />
61
+ <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
62
+ <MMenu
63
+ :model="menuModel"
64
+ :collapsed="siderCollapsed"
65
+ :collapsed-width="72"
66
+ selected-key="users"
67
+ />
57
68
  </MLayoutSider>
58
69
 
59
70
  <MLayout>
@@ -0,0 +1,6 @@
1
+ html,
2
+ body,
3
+ #app {
4
+ height: 100%;
5
+ margin: 0;
6
+ }