dsh-design-studio 0.4.0 → 0.5.0

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/lib/preset.js CHANGED
@@ -1,5 +1,22 @@
1
1
  /**
2
- * dsh-design-studio/preset:设计模式那一行。就是 DSH 自带的 Agent 预设,
3
- * 换个入口好让 DSH「插件」页里显示成设计模式(名字和说明见 locale/preset/),而不是通用的「Agent 预设定义」
2
+ * dsh-design-studio/preset:设计模式那一行。和 DSH 自带的 Agent 预设(@deepseek-ai/dsh-agent-preset)一模一样,
3
+ * 换个入口好让 DSH「插件」页里显示成设计模式(名字和说明见 locale/preset/),而不是通用的「Agent 预设定义」。
4
+ *
5
+ * 不直接 re-export 那个包:用 link: 装、插件文件夹在 DSH 目录外面时(本地开发),从这里找不到 @deepseek-ai/* 包,
6
+ * 这一行就起不来、模式列表里没有设计模式。官方实现只有十几行,照抄;两个键是 cordis 的全局符号(Symbol.for)。
4
7
  */
5
- export { default } from '@deepseek-ai/dsh-agent-preset'
8
+ export default class DesignPreset {
9
+ static inject = ['agentPresets']
10
+ // 子插件的配置原样留着,等各自激活时再解析(官方的 EntryGroup.key)
11
+ static [Symbol.for('cordis.group')] = true
12
+
13
+ constructor(ctx, config) {
14
+ this.ctx = ctx
15
+ this.config = config
16
+ }
17
+
18
+ // 官方的 Service.init:注册上就算起来了,停用时 yield 出去的反注册会被调用
19
+ async * [Symbol.for('cordis.init')]() {
20
+ yield await this.ctx.agentPresets.register(this.config)
21
+ }
22
+ }
package/lib/serve.js CHANGED
@@ -25,7 +25,7 @@ const MAX_HTML = 8 * 1024 * 1024 // 要注入脚本的 HTML 最多读这么大
25
25
  const UP_HOSTS = ['127.0.0.1', '::1'] // 开发服务器可能只听其中一个
26
26
  const DEBOUNCE_MS = 120
27
27
 
28
- const MIME = {
28
+ export const MIME = {
29
29
  '.html': 'text/html; charset=utf-8',
30
30
  '.htm': 'text/html; charset=utf-8',
31
31
  '.css': 'text/css; charset=utf-8',
@@ -56,7 +56,7 @@ const MIME = {
56
56
  }
57
57
 
58
58
  // 改动通知里不算的:版本库、依赖、系统垃圾
59
- const EXPORTS = /^(exports([\\/][^\\/]+\.png)?|refs([\\/][^\\/]+\.(png|jpe?g|gif|webp))?)$/i // 导出的图片、参考图(和第一次建的 exports / refs 目录):不算设计改了
59
+ const EXPORTS = /^(exports([\\/][^\\/]+\.(png|pdf|pptx))?|refs([\\/][^\\/]+\.(png|jpe?g|gif|webp))?)$/i // 导出的图片 / PDF / PPT、参考图(和第一次建的 exports / refs 目录):不算设计改了
60
60
  const IGNORED = /(^|[\\/])(\.git|node_modules|\.DS_Store|\.next|\.cache|\.turbo)([\\/]|$)/
61
61
 
62
62
  /** 只认本机名 + 自己的端口(防 DNS 重绑定:外站把域名解析到 127.0.0.1 也进不来) */
@@ -65,11 +65,26 @@ export function hostOk(host, port) {
65
65
  return h === `127.0.0.1:${port}` || h === `localhost:${port}` || h === `[::1]:${port}`
66
66
  }
67
67
 
68
- /** 在 </body> 前(没有就在末尾)插入选取脚本 */
68
+ /**
69
+ * 最先跑的一小段:记下页面脚本报错、没载出来的样式表 / 脚本(设计体检报给模型)。
70
+ * picker 在 </body> 前,等它装上,前面脚本的报错早过去了
71
+ */
72
+ export const ERR_CATCHER = '<script>(()=>{if(window.__dshErrs)return;const L=window.__dshErrs=[];const add=(x)=>{if(L.length<30)L.push(x)};' +
73
+ 'addEventListener("error",(e)=>{const t=e.target;if(t&&t!==window&&t.tagName){add({res:1,tag:t.tagName.toLowerCase(),src:String(t.currentSrc||t.src||t.href||"").slice(0,300)})}' +
74
+ 'else add({msg:String(e.message||"").slice(0,300),src:String(e.filename||"").slice(0,300),line:e.lineno||0})},true);' +
75
+ 'addEventListener("unhandledrejection",(e)=>{const r=e.reason;add({msg:String((r&&r.message)||r).slice(0,300),src:"",line:0})})})()</script>'
76
+
77
+ /** 放进 <head> 最前面;没写 <head> 就放在 <html> / <!doctype> 后面(放到 doctype 前面页面会进怪异模式) */
78
+ export function headInsert(html, tag) {
79
+ const m = /<head\b[^>]*>/i.exec(html) || /<html\b[^>]*>/i.exec(html) || /^\s*<!doctype\b[^>]*>/i.exec(html)
80
+ return m ? html.slice(0, m.index + m[0].length) + tag + html.slice(m.index + m[0].length) : tag + html
81
+ }
82
+
83
+ /** 在 </body> 前(没有就在末尾)插入选取脚本,<head> 最前面放报错记录 */
69
84
  export function injectPicker(html) {
70
85
  const tag = `<script src="${INTERNAL}picker.js"></script>`
71
86
  const i = html.toLowerCase().lastIndexOf('</body>')
72
- return i >= 0 ? html.slice(0, i) + tag + html.slice(i) : html + tag
87
+ return headInsert(i >= 0 ? html.slice(0, i) + tag + html.slice(i) : html + tag, ERR_CATCHER)
73
88
  }
74
89
 
75
90
  /** 真实路径在不在根目录(也取真实路径)里面 */
@@ -142,7 +157,7 @@ export function trustedOrigins(hosts) {
142
157
 
143
158
  /**
144
159
  * @param opts.log 出错时写日志
145
- * @param opts.onChange (key, file) 目录里有文件改了(file 目标)
160
+ * @param opts.onChange (key, file, files) 目录里有文件改了(file 目标;files:这一批的相对路径)
146
161
  * @param opts.history 设计版本(lib/history.js):/__dsh_design__/v/<版本>/<文件> 给出那一版的文件
147
162
  * @param opts.parents () → 还有哪些 DSH 页面的源能嵌预览(DSH 网页端的地址)
148
163
  */
@@ -261,10 +276,11 @@ export function createHub({ log = () => {}, onChange = () => {}, history = null,
261
276
  batch.add(rel.split(path.sep).join('/'))
262
277
  clearTimeout(timer)
263
278
  timer = setTimeout(() => {
264
- const files = [...batch].slice(0, 50)
279
+ // 一批改了太多文件就不列了(空的 = 不知道改了啥,页面整屏刷新),列一半会漏掉这一屏用到的
280
+ const files = batch.size > 50 ? [] : [...batch]
265
281
  batch.clear()
266
282
  for (const c of entry.clients) c.write(`event: change\ndata: ${JSON.stringify({ file: last, files })}\n\n`)
267
- try { onChange(entry.key, path.join(entry.root, last)) } catch (e) { log(e) }
283
+ try { onChange(entry.key, path.join(entry.root, last), files) } catch (e) { log(e) }
268
284
  }, DEBOUNCE_MS)
269
285
  })
270
286
  entry.watcher.on('error', (e) => log(e))
package/lib/tool.js CHANGED
@@ -1,12 +1,15 @@
1
1
  /**
2
- * dsh-design-studio/tool:把 preview_page 工具挂给模型。
3
- * 放在 bundle 顶层 → 所有模式都能用;放进设计模式 → 只挂在那个模式里。工具本身见 index.js 的 previewTool
2
+ * dsh-design-studio/tool:把 preview_page、check_design、look_design 三个工具挂给模型。
3
+ * 放在 bundle 顶层 → 所有模式都能用;放进设计模式 → 只挂在那个模式里。工具本身见 index.js 的 previewTool / checkTool / lookTool
4
4
  */
5
- import { previewTool } from './index.js'
5
+ import { previewTool, checkTool, lookTool } from './index.js'
6
6
 
7
7
  export const name = 'dsh-design-studio-tool'
8
8
  export const inject = ['tools']
9
9
 
10
10
  export function apply(ctx) {
11
11
  ctx.tools.register({ ...previewTool })
12
+ // 截的图要存进 DSH 的附件库(ctx.get('attachments')),把这一行的 ctx 带进去
13
+ ctx.tools.register({ ...checkTool, execute: (args, exec) => checkTool.execute(args, exec, ctx) })
14
+ ctx.tools.register({ ...lookTool, execute: (args, exec) => lookTool.execute(args, exec, ctx) })
12
15
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "Preview tool (other modes)",
4
- "description": "Lets Standard, Creator and other modes use preview_page to open pages in the Design Studio sidebar. The Design mode has its own copy and ignores this switch."
3
+ "title": "Preview, check and look tools (other modes)",
4
+ "description": "Lets Standard, Creator and other modes use preview_page (open pages in the Design Studio sidebar), check_design (measure design problems at real sizes) and look_design (real-size screenshots for the model). The Design mode has its own copy and ignores this switch."
5
5
  }
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "预览工具(其他模式)",
4
- "description": "让标准、创造等其他模式也能用 preview_page,把页面打开到右侧「设计工作台」。设计模式里自带,不受这个开关影响。"
3
+ "title": "预览、体检和截图工具(其他模式)",
4
+ "description": "让标准、创造等其他模式也能用 preview_page(把页面打开到右侧「设计工作台」)、check_design(按真实尺寸量一遍设计问题)和 look_design(按真实尺寸截图给模型看)。设计模式里自带,不受这个开关影响。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-design-studio",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test --test-concurrency=1 test/*.mjs"
@@ -4,6 +4,15 @@
4
4
 
5
5
  ## 流程
6
6
 
7
+ 0. **新做一套设计:先问清,先出方向**。
8
+
9
+ - **什么时候跳过**:改已有的设计;用户说了「直接做」「不用问」;或者需求已经够具体(做什么、给谁用、什么风格或配色、要哪几屏都说了)。这时不问、也不出三个方向,第一句话说清你按什么理解开工(「按温暖手作风格做首页、详情、购物车三屏」),直接从第 1 步做全套。拿不准算不算够具体,就只问缺的那一两项。
10
+
11
+ - **问**:只问最关键的两三个问题,一次问完:给谁用、想要什么感觉(给三个具体选项,从工作台的风格卡里挑最合场景的三个:极简白、温暖手作、杂志编辑、深色高级、活泼年轻、清新自然、专业科技、玻璃质感、柔和立体)、有没有必须有的内容或品牌色。有 `ask_user_question` 工具就用它问(带选项),没有就在回复里问完、停下等回答。用户说「你定」就按场景替他定。
12
+ - **用户点了风格卡**(消息以〔风格〕开头,带着配色、字体、形状和要点):感觉已经定了,不用再问感觉,只问还缺的(给谁用、必须有的内容);三个方向都按这套配色和字体来,在版式和信息重点上拉开。
13
+ - **出三个方向**:先只做最能代表这套设计的一屏(通常是首页或封面),做三个明显不同的方向:`home-a.html`、`home-b.html`、`home-c.html`。用户已经挑了感觉(比如「温暖手作」),三个方向就都在这个感觉里拉开:版式、信息重点、字体搭配、配色深浅各不相同(比如「大图海报式 / 卡片列表式 / 一步下单式」),不要再出两个他没选的风格;没挑或说「你定」,三个方向就是三种不同的感觉。不管哪种,都不是同一个版式换颜色。这时还不建 `tokens.css`,每个方向的设计变量写在自己文件的 `<style>` 里(`:root{…}`)。`<title>` 写成「方向 A · 温暖手作」这样,画布上一眼分得清。写完第一个就调 `preview_page`,另外两个会并排出现。
14
+ - **等用户挑**:一两句话说清三个方向各自的特点,问用户要哪个(也可以混搭:「A 的配色 + C 的版式」)。用户也可能直接在画布上点「就要这个」,消息以〔选定方向〕开头,照里面说的收尾。挑定以后:把选中方向的变量搬进 `tokens.css`,那个文件改成正式的名字(`home.html`)并改用 `tokens.css`,另外两个移进 `drafts/` 子目录(别删),然后按下面的流程做全套。
15
+
7
16
  1. **先定类型和画幅**(拿不准就按用户说的场景选):
8
17
 
9
18
  | kind | 画幅 | 外框 | 细则 |
@@ -19,20 +28,48 @@
19
28
 
20
29
  2. **先写设计变量**:在设计目录建 `tokens.css`(起手模板 `tokens.css` 就在这个技能目录里,复制过去再按品牌改),颜色、字体、字号阶梯、间距、圆角、阴影都从这里取。每屏都 `<link rel="stylesheet" href="tokens.css">`。之后要统一改风格,改这一个文件,所有屏一起变。
21
30
 
22
- 3. **一屏一个 HTML 文件,放同一个目录**,按用户会看的顺序一屏一屏写(画布按创建顺序排:登录 → 首页 → 详情 → …)。文件名用英文小写(`login.html`、`home.html`),`<title>` 写这屏的中文名(画布上显示它)。每屏 `<head>` 里写:
31
+ 3. **一屏一个 HTML 文件,放同一个目录**,按用户会看的顺序一屏一屏写(画布按创建顺序排:登录 → 首页 → 详情 → …)。文件名用英文小写(`login.html`、`home.html`),`<title>` 写这屏的中文名(画布上显示它)。临时试验的页面用 `_` 开头(`_test.html`),不会上画布。每屏 `<head>` 里写:
23
32
 
24
33
  ```html
25
34
  <meta name="dsh-design" content="app"> <!-- 类型;海报可带尺寸:poster 1080x1920 -->
26
35
  ```
27
36
 
37
+ **几屏共用的东西只写一份**:样式放 `tokens.css` 和一个公共的 `app.css`;图标、书封这类 SVG 放进一个 `icons.svg`(每个图标一个 `<symbol id="…">`),页面里用 `<svg><use href="icons.svg#cart"></use></svg>` 引用——换风格时改一处所有屏一起变,不会漏改。状态栏、标签栏这种 HTML 结构每屏照抄一份,保持一模一样。
38
+
28
39
  **几屏之间用真链接串起来**,让它成为点得动的原型:按钮、卡片、标签栏、返回箭头都写成 `<a href="detail.html">`(或 `data-href="detail.html"`)。返回也写具体去哪一屏(`<a href="home.html">`),不要用 `history.back()`。右侧画布会从按钮画一条连线到目标屏,「演示」时点按钮就跳过去(带过场动画),手机扫码看时也点得动。
29
40
 
30
41
  4. **写完第一屏就调 `preview_page`**(`target` 填这个文件,`kind` 填类型),用户从第一屏起就能看着它长出来;后面的屏写到同一目录会自动并排出现,不用再调。
31
42
 
32
- 5. **写完跑一遍下面的「自查」**,有问题直接改,最后用一两句话告诉用户做了哪几屏、风格要点。
43
+ 5. **写完先量、再看,然后收尾**:
44
+ - **量**:调 `check_design`。它在电脑上用无头浏览器按真实尺寸打开每一屏实测(文字截断、重叠、对比度、点击区、字号、变形、行太长、固定栏压住内容、脚本报错、文件没载出来、跳到不存在的页面……),返回量了哪几屏、每种尺寸下页面多高、顶栏 / 底栏离内容多远,和问题清单(哪一屏、哪个元素、具体数字);严重的问题附了那一块的局部截图(清单里写「看图 几」,红框是出问题的元素、框上标了差多少),对着图改,不用再另外截;叠字写了「可能是 … 的 margin-top」这类原因和规则所在的行,先看那里。App 顺手量一下小屏和大屏:`sizes: ["375x667", "430x932"]`;网页量手机宽:`sizes: ["390", "1280"]`。
45
+ - **看**:调 `look_design` 看一眼真实渲染的截图,检查量不出来的东西(层次、留白、配色、图片、整体观感);底栏多的 App 用 `at: "bottom"` 看滚到底的样子,长页面用 `at: "full"`,只看某一块用 `selector`。
46
+ - **改**:照第 7 步的办法改,改完只重量改过的屏(`screens` 只填那几屏)。
47
+ - **什么时候算做完**:`check_design` 没有问题(或剩下的你判断不该改,说得出理由),每屏都看过一眼截图。量 / 看 / 改最多三轮:三轮后还有的,照实列给用户,别一直磨。不要自己另写检查页、起服务、开浏览器截图——上面两个工具就是干这个的,又快又准。
48
+ - 最后用一两句话告诉用户做了哪几屏、风格要点、还剩什么没解决。
33
49
 
34
50
  6. **用户点选后让你改**(消息里有〔选中元素〕/〔选中区域〕):按里面的文件和 CSS 路径找到源码改那一处;如果是颜色、字号、圆角这类风格问题,优先改 `tokens.css` 里的变量,让所有屏保持一致。
35
51
 
52
+ **用户在工作台里手调过样式**:元素上的 `style="…"` 多半是用户用「调样式」拖出来的,值是用户要的,别随手删、也别改回去;整理代码时可以挪进对应的样式规则(值不变)。消息以〔调样式〕开头时,按里面的元素、选择器和「原来 → 现在」写进样式表,值是 `var(--x)` 的就用那个变量;同类元素(比如所有主按钮)该一起改就一起改,改完说一句。
53
+
54
+ **换风格**(〔风格〕后面说「把现在的设计换成这个风格」):先改 `tokens.css`(配色、字体、圆角、阴影按消息里的来),页面里写死的颜色换成变量,再按「要点」调细节;布局和文案不动。改完调 `check_design` 量一遍(换了配色最容易出对比度问题)。
55
+
56
+ 7. **改体检出来的问题**(`check_design` 的结果,或用户发来的〔设计体检〕):右侧工作台在渲染好的页面上实测出来的,按里面的文件、选择器和数字逐条改,没列出来的地方别动:
57
+ - 对比度不够:调深文字色(或调浅底色),能改 `tokens.css` 里的 `muted`、主色上的文字色就改变量;
58
+ - 文字被截断 / 超出框:放宽容器、允许换行或去掉写死的高度,别靠缩小字号硬塞;
59
+ - 文字叠在一起:检查绝对定位和负边距,让它们回到正常排版;
60
+ - 点击区太小:加内边距或 `min-height` / `min-width`,图标按钮的点击区可以比图标本身大;
61
+ - 超出屏幕:找写死的宽度(`width: 600px`、`100vw` 加内边距),改成 `max-width: 100%` 或自适应;
62
+ - 字太小、字号太杂:收到字号阶梯里的几档;
63
+ - 图片被拉变形:加 `object-fit: cover`(要完整显示就 `contain`),或按原图比例改宽高;
64
+ - 一行太长:给正文容器加 `max-width`(中文约 `36em`,英文约 `65ch`),别靠缩小字号。
65
+ - 文字被挤断行:按钮、价格、标签这种短字给足宽度,或加 `white-space: nowrap`(在 flex 里再加 `flex-shrink: 0`);一句短话最后一行只剩一两个字,就调宽度或改字。
66
+ - 被固定栏压住 / 离固定栏太近:给内容留出栏的高度——底栏是 `position: fixed` 的,给 `body` 或内容容器加 `padding-bottom: calc(栏高 + 16px)`(App 再加 `env(safe-area-inset-bottom)`);内容在能滚的 `main` 里,就给 `main` 加 `padding-bottom`。顶栏同理用 `padding-top`。
67
+ - 页面脚本报错:按报的文件和行号修;纯展示用的设计稿能不写脚本就不写。
68
+ - 文件没载出来:路径写错了(大小写、相对路径),或者文件还没建,补上或改对。
69
+ - 跳到的页面不存在:补上那一屏,或者把链接改成已有的屏。
70
+
71
+ 改完说一句改了哪几处。这一轮结束后右侧栏会自己再量一遍,按钮上的数字没了就是都改好了。
72
+
36
73
  ## 设计原则
37
74
 
38
75
  **层级**:每屏只有一个视觉焦点(最大的字 / 最亮的按钮)。用字号、字重、颜色深浅三档拉开层级,不要靠边框和分割线堆结构。主操作一个,次操作弱化成描边或文字按钮。
@@ -86,5 +123,6 @@
86
123
  - [ ] 文案是真实的中文内容,没有占位文字、没有错别字
87
124
  - [ ] 图标同一套风格,没有 emoji 图标
88
125
  - [ ] 画幅对:App 留出顶部 54px 状态栏和底部 34px 横条;海报 / 幻灯片内容在安全区内不溢出
126
+ - [ ] 文字没有被截断、没有伸出框、没有叠在一起;页面没有比屏幕宽;滚到底最后的内容没被底栏压住(`check_design` 实测这几项)
89
127
  - [ ] 多屏之间导航、按钮、卡片样式一致(像同一个产品)
90
128
  - [ ] 没有上面「AI 味」清单里的东西
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 画幅
4
4
 
5
- - 手机按 390×844(iPhone 15/16)设计,页面宽度就写满 390,不要居中一个小盒子。`<meta name="viewport" content="width=device-width,initial-scale=1">`。
5
+ - 手机按 390×844(iPhone 15/16)设计,页面宽度跟着视口走(`width: 100%`),不要居中一个小盒子。`<meta name="viewport" content="width=device-width,initial-scale=1">`。别写死 `width: 390px`、也别给 body 加 `max-width` 再居中:工作台已经按 390 宽显示、套好手机外框,多尺寸视图还会拿 375 / 430 宽来看,写死就露馅。也别为了这个另开浏览器截图——要检查就调 `check_design`。
6
6
  - 顶部 54px 是状态栏 + 灵动岛:自己画一条状态栏(时间 9:41、信号、电量),中间空着——灵动岛预览的外框会画,页面里别再画一个(不然换尺寸看时会出现两个)。内容从 54px 以下开始。
7
7
  - 底部 34px 是横条(home indicator)区:底部的标签栏 / 按钮要把它让出来(标签栏总高约 83px)。
8
8
  - 左右边距 16–24px;卡片之间 12–16px。