dsh-design-studio 0.9.0 → 0.11.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.
Files changed (95) hide show
  1. package/README.en.md +6 -4
  2. package/README.md +6 -4
  3. package/assets/demo.gif +0 -0
  4. package/assets/hero-sm.jpg +0 -0
  5. package/cordis.patch.yml +5 -0
  6. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
  7. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
  8. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
  9. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
  10. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
  11. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
  12. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
  13. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
  14. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
  15. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
  16. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
  17. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
  18. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
  19. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
  20. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
  21. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
  22. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
  23. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
  24. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
  25. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
  26. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
  27. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
  28. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
  29. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
  30. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
  31. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
  32. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
  33. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
  34. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
  35. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
  36. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
  37. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
  38. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
  39. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
  40. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
  41. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
  42. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
  43. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
  44. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
  45. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
  46. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
  47. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
  48. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
  49. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
  50. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
  51. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
  52. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
  53. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
  54. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
  55. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
  56. package/gallery-seed/index.json +1 -0
  57. package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
  58. package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
  59. package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
  60. package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
  61. package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
  62. package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
  63. package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
  64. package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
  65. package/lib/browser.js +23 -9
  66. package/lib/bundle.js +12 -3
  67. package/lib/client.js +3106 -111
  68. package/lib/core.js +44 -11
  69. package/lib/gallery/css.js +230 -0
  70. package/lib/gallery/html.js +313 -0
  71. package/lib/gallery/index.js +22 -0
  72. package/lib/gallery/market-routes.js +306 -0
  73. package/lib/gallery/market.js +623 -0
  74. package/lib/gallery/pack.js +311 -0
  75. package/lib/gallery/policy.js +1560 -0
  76. package/lib/gallery/qrfind.js +181 -0
  77. package/lib/gallery/render.js +148 -0
  78. package/lib/gallery/routes.js +360 -0
  79. package/lib/gallery/tar.js +118 -0
  80. package/lib/gallery/use.js +125 -0
  81. package/lib/gallery/zip.js +212 -0
  82. package/lib/handoff.js +291 -29
  83. package/lib/history.js +3 -1
  84. package/lib/index.js +118 -24
  85. package/lib/inspire.js +142 -0
  86. package/lib/offline.js +17 -0
  87. package/lib/picker.js +425 -32
  88. package/lib/serve.js +88 -11
  89. package/lib/works.js +238 -0
  90. package/locale/en.json +2 -1
  91. package/locale/offline/en.json +6 -0
  92. package/locale/offline/zh.json +6 -0
  93. package/locale/zh.json +2 -1
  94. package/package.json +10 -3
  95. package/skills/ui-design/SKILL.md +9 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * 极简 zip:打包(只存储不压缩,CRC32 自己算)和解包(存储 / deflate),不加依赖。
3
+ *
4
+ * 解包是给「朋友传来的 zip」用的,按不可信数据处理:
5
+ * - 只收普通文件(目录项跳过);软链接、加密、分卷、zip64 一律拒
6
+ * - 路径:不能是绝对路径、不能有 ..、反斜杠、NUL、控制字符;Mac 打包带的 __MACOSX/、.DS_Store 直接跳过
7
+ * - 个数、单个大小、总大小都有上限,解压时按上限截断(防 zip 炸弹:声明的大小不可信,以实际解出来的为准)
8
+ * 规则(扩展名白名单、内容检查)在 policy.js 的 checkPack 里,这里只管拆包本身安全
9
+ */
10
+
11
+ import zlib from 'node:zlib'
12
+
13
+ const CRC_TABLE = (() => {
14
+ const t = new Uint32Array(256)
15
+ for (let n = 0; n < 256; n++) {
16
+ let c = n
17
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
18
+ t[n] = c >>> 0
19
+ }
20
+ return t
21
+ })()
22
+
23
+ export function crc32(buf) {
24
+ let c = 0xffffffff
25
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8)
26
+ return (c ^ 0xffffffff) >>> 0
27
+ }
28
+
29
+ /** DOS 时间(zip 里的修改时间) */
30
+ function dosTime(d) {
31
+ const y = Math.max(1980, d.getFullYear())
32
+ return {
33
+ time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
34
+ date: ((y - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
35
+ }
36
+ }
37
+
38
+ /** [{ path, data: Buffer }] → zip 的 Buffer(文件名 UTF-8,只存储) */
39
+ export function writeZip(files, { now = new Date() } = {}) {
40
+ const { time, date } = dosTime(now)
41
+ const locals = []
42
+ const centrals = []
43
+ let offset = 0
44
+ for (const f of files) {
45
+ const name = Buffer.from(String(f.path), 'utf8')
46
+ const data = Buffer.isBuffer(f.data) ? f.data : Buffer.from(String(f.data), 'utf8')
47
+ const crc = crc32(data)
48
+ const lh = Buffer.alloc(30)
49
+ lh.writeUInt32LE(0x04034b50, 0)
50
+ lh.writeUInt16LE(20, 4) // 解压要的版本
51
+ lh.writeUInt16LE(0x0800, 6) // 第 11 位:文件名是 UTF-8
52
+ lh.writeUInt16LE(0, 8) // 只存储
53
+ lh.writeUInt16LE(time, 10)
54
+ lh.writeUInt16LE(date, 12)
55
+ lh.writeUInt32LE(crc, 14)
56
+ lh.writeUInt32LE(data.length, 18)
57
+ lh.writeUInt32LE(data.length, 22)
58
+ lh.writeUInt16LE(name.length, 26)
59
+ lh.writeUInt16LE(0, 28)
60
+ locals.push(lh, name, data)
61
+ const ch = Buffer.alloc(46)
62
+ ch.writeUInt32LE(0x02014b50, 0)
63
+ ch.writeUInt16LE(0x031e, 4) // 做出来的版本:Unix
64
+ ch.writeUInt16LE(20, 6)
65
+ ch.writeUInt16LE(0x0800, 8)
66
+ ch.writeUInt16LE(0, 10)
67
+ ch.writeUInt16LE(time, 12)
68
+ ch.writeUInt16LE(date, 14)
69
+ ch.writeUInt32LE(crc, 16)
70
+ ch.writeUInt32LE(data.length, 20)
71
+ ch.writeUInt32LE(data.length, 24)
72
+ ch.writeUInt16LE(name.length, 28)
73
+ ch.writeUInt16LE(0, 30)
74
+ ch.writeUInt16LE(0, 32)
75
+ ch.writeUInt16LE(0, 34)
76
+ ch.writeUInt16LE(0, 36)
77
+ ch.writeUInt32LE((0o100644 << 16) >>> 0, 38) // 普通文件 rw-r--r--
78
+ ch.writeUInt32LE(offset, 42)
79
+ centrals.push(ch, name)
80
+ offset += 30 + name.length + data.length
81
+ }
82
+ const cd = Buffer.concat(centrals)
83
+ const end = Buffer.alloc(22)
84
+ end.writeUInt32LE(0x06054b50, 0)
85
+ end.writeUInt16LE(files.length, 8)
86
+ end.writeUInt16LE(files.length, 10)
87
+ end.writeUInt32LE(cd.length, 12)
88
+ end.writeUInt32LE(offset, 16)
89
+ return Buffer.concat([...locals, cd, end])
90
+ }
91
+
92
+ export const ZIP_LIMITS = { files: 200, file: 4 * 1024 * 1024, total: 12 * 1024 * 1024, zip: 8 * 1024 * 1024 }
93
+
94
+ class ZipError extends Error {
95
+ constructor(code, x = '') {
96
+ super(`${code}${x ? ': ' + x : ''}`)
97
+ this.code = code
98
+ this.x = x
99
+ }
100
+ }
101
+
102
+ /** Mac / Windows 打包时顺手塞进去的东西:不算设计文件,直接跳过 */
103
+ const JUNK = /(^|\/)(__MACOSX|\.DS_Store|Thumbs\.db|desktop\.ini)(\/|$)|(^|\/)\._/i
104
+
105
+ /** zip 里的路径 → 干净的相对路径;不安全给 null */
106
+ function safeName(raw) {
107
+ if (/[\x00-\x1f\x7f\\]/.test(raw)) return null
108
+ if (raw.startsWith('/') || /^[A-Za-z]:/.test(raw)) return null
109
+ const parts = raw.split('/').filter((p) => p !== '' && p !== '.')
110
+ if (!parts.length || parts.some((p) => p === '..')) return null
111
+ return parts.join('/')
112
+ }
113
+
114
+ /**
115
+ * zip 的 Buffer → { files: [{ path, data }], skipped: [路径] };不安全 / 坏的抛 ZipError(code:bad / big / count / link / crypt / method / zip64 / path / dup)
116
+ * 包外面套了一层同名文件夹(「晨光咖啡/home.html」)的,自动去掉这一层
117
+ */
118
+ export function readZip(buf, limits = ZIP_LIMITS) {
119
+ if (!Buffer.isBuffer(buf) || buf.length < 22) throw new ZipError('bad')
120
+ if (buf.length > limits.zip) throw new ZipError('big')
121
+ // 找中央目录结尾(最后 64KB + 22 字节内)
122
+ let eocd = -1
123
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 0xffff); i--) {
124
+ if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break }
125
+ }
126
+ if (eocd < 0) throw new ZipError('bad')
127
+ const disk = buf.readUInt16LE(eocd + 4)
128
+ const cdDisk = buf.readUInt16LE(eocd + 6)
129
+ const count = buf.readUInt16LE(eocd + 10)
130
+ const cdSize = buf.readUInt32LE(eocd + 12)
131
+ const cdOff = buf.readUInt32LE(eocd + 16)
132
+ if (disk !== 0 || cdDisk !== 0) throw new ZipError('bad', 'multi-disk')
133
+ if (count === 0xffff || cdOff === 0xffffffff || cdSize === 0xffffffff) throw new ZipError('zip64')
134
+ if (cdOff + cdSize > eocd) throw new ZipError('bad')
135
+ if (count > limits.files * 2) throw new ZipError('count')
136
+
137
+ const entries = []
138
+ let p = cdOff
139
+ for (let n = 0; n < count; n++) {
140
+ if (p + 46 > eocd || buf.readUInt32LE(p) !== 0x02014b50) throw new ZipError('bad')
141
+ const flags = buf.readUInt16LE(p + 8)
142
+ const method = buf.readUInt16LE(p + 10)
143
+ const crc = buf.readUInt32LE(p + 16)
144
+ const csize = buf.readUInt32LE(p + 20)
145
+ const usize = buf.readUInt32LE(p + 24)
146
+ const nlen = buf.readUInt16LE(p + 28)
147
+ const xlen = buf.readUInt16LE(p + 30)
148
+ const clen = buf.readUInt16LE(p + 32)
149
+ const made = buf.readUInt16LE(p + 4) >> 8
150
+ const ext = buf.readUInt32LE(p + 38)
151
+ const loff = buf.readUInt32LE(p + 42)
152
+ if (p + 46 + nlen > eocd) throw new ZipError('bad')
153
+ const nameBuf = buf.subarray(p + 46, p + 46 + nlen)
154
+ // 文件名:标了 UTF-8 的按 UTF-8;没标的也先按 UTF-8 认(Mac 自带的压缩就是这样),认不出来按 latin1 当乱码路径,后面会被拒
155
+ const name = new TextDecoder('utf-8', { fatal: false }).decode(nameBuf)
156
+ p += 46 + nlen + xlen + clen
157
+ const isDir = name.endsWith('/')
158
+ if (JUNK.test(name)) continue
159
+ if (isDir) continue
160
+ if (flags & 1) throw new ZipError('crypt', name)
161
+ if (made === 3) {
162
+ const mode = (ext >>> 16) & 0o170000
163
+ if (mode === 0o120000) throw new ZipError('link', name) // 软链接
164
+ if (mode && mode !== 0o100000) throw new ZipError('link', name) // 设备文件、管道之类
165
+ }
166
+ if (csize === 0xffffffff || usize === 0xffffffff || loff === 0xffffffff) throw new ZipError('zip64')
167
+ if (method !== 0 && method !== 8) throw new ZipError('method', name)
168
+ const clean = safeName(name)
169
+ if (!clean) throw new ZipError('path', name)
170
+ if (usize > limits.file) throw new ZipError('big', clean)
171
+ entries.push({ name: clean, method, crc, csize, usize, loff, flags })
172
+ }
173
+ if (entries.length > limits.files) throw new ZipError('count')
174
+
175
+ const files = []
176
+ const seen = new Set()
177
+ let total = 0
178
+ for (const e of entries) {
179
+ if (e.loff + 30 > buf.length || buf.readUInt32LE(e.loff) !== 0x04034b50) throw new ZipError('bad', e.name)
180
+ const start = e.loff + 30 + buf.readUInt16LE(e.loff + 26) + buf.readUInt16LE(e.loff + 28)
181
+ if (start + e.csize > buf.length) throw new ZipError('bad', e.name)
182
+ const raw = buf.subarray(start, start + e.csize)
183
+ let data
184
+ if (e.method === 0) {
185
+ data = Buffer.from(raw)
186
+ } else {
187
+ try {
188
+ // 以实际解出来的为准:超过单个上限就停(maxOutputLength 超了会抛)
189
+ data = zlib.inflateRawSync(raw, { maxOutputLength: limits.file })
190
+ } catch {
191
+ throw new ZipError('big', e.name)
192
+ }
193
+ }
194
+ if (data.length !== e.usize || crc32(data) !== e.crc) throw new ZipError('bad', e.name)
195
+ total += data.length
196
+ if (total > limits.total) throw new ZipError('big')
197
+ const key = e.name.toLowerCase()
198
+ if (seen.has(key)) throw new ZipError('dup', e.name)
199
+ seen.add(key)
200
+ files.push({ path: e.name, data })
201
+ }
202
+ // 外面套了一层文件夹:所有文件都在同一个顶层目录里 → 去掉这一层
203
+ const tops = new Set(files.map((f) => (f.path.includes('/') ? f.path.split('/')[0] : '')))
204
+ let wrapper = ''
205
+ if (files.length && tops.size === 1 && !tops.has('')) {
206
+ wrapper = [...tops][0]
207
+ for (const f of files) f.path = f.path.slice(wrapper.length + 1)
208
+ }
209
+ return { files, wrapper }
210
+ }
211
+
212
+ export { ZipError }