modelstage 0.0.0-stage → 0.5.28

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 (281) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +137 -0
  5. package/config/diag-upload.example.json +7 -0
  6. package/config/models/live2d.assetbundls.json +1 -0
  7. package/config/models/live2d.azurlane.json +1 -0
  8. package/config/models/live2d.azurlane_bundle.json +1 -0
  9. package/config/models/live2d.azurlane_spine.json +1 -0
  10. package/config/models/live2d.bd2.json +1 -0
  11. package/config/models/live2d.bluearchive.json +1 -0
  12. package/config/models/live2d.live2d.json +1 -0
  13. package/config/models/live2d.nikke.json +1 -0
  14. package/config/models/live2d.nikke_2.json +1 -0
  15. package/config/models/live2d.npc.json +1 -0
  16. package/config/models/spine.assetbundls.json +1 -0
  17. package/config/models/spine.azurlane.json +1 -0
  18. package/config/models/spine.azurlane_bundle.json +1 -0
  19. package/config/models/spine.azurlane_spine.json +1 -0
  20. package/config/models/spine.bd2.json +1 -0
  21. package/config/models/spine.bluearchive.json +1 -0
  22. package/config/models/spine.live2d.json +1 -0
  23. package/config/models/spine.nikke.json +1 -0
  24. package/config/models/spine.nikke_2.json +1 -0
  25. package/config/models/spine.npc.json +1 -0
  26. package/config/names/NIKKE.json +1058 -0
  27. package/config/names/README.md +47 -0
  28. package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
  29. package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
  30. package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
  31. package/config/plugins.json +13 -0
  32. package/config/resources.example.json +40 -0
  33. package/package.json +63 -4
  34. package/plugins/unity-live2d-bundle/README.md +98 -0
  35. package/plugins/unity-live2d-bundle/index.js +891 -0
  36. package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
  37. package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
  38. package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
  39. package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
  40. package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
  41. package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
  42. package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
  43. package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
  44. package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  45. package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
  46. package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
  47. package/plugins/unity-live2d-bundle/lib/png.js +424 -0
  48. package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
  49. package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
  50. package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
  51. package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
  52. package/plugins/unity-live2d-bundle/plugin.json +81 -0
  53. package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
  54. package/plugins/unity-spine-bundle/index.js +535 -0
  55. package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
  56. package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
  57. package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
  58. package/plugins/unity-spine-bundle/lib/errors.js +79 -0
  59. package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
  60. package/plugins/unity-spine-bundle/lib/extract.js +242 -0
  61. package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  62. package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
  63. package/plugins/unity-spine-bundle/lib/png.js +424 -0
  64. package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
  65. package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
  66. package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
  67. package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
  68. package/plugins/unity-spine-bundle/plugin.json +75 -0
  69. package/src/server/README.md +69 -0
  70. package/src/server/assets/browse.mjs +56 -0
  71. package/src/server/assets/bundle-probe.mjs +94 -0
  72. package/src/server/assets/probe.mjs +184 -0
  73. package/src/server/core/config.mjs +173 -0
  74. package/src/server/core/env-num.mjs +60 -0
  75. package/src/server/core/env.mjs +49 -0
  76. package/src/server/core/fs-portable.mjs +37 -0
  77. package/src/server/core/log.mjs +107 -0
  78. package/src/server/core/paths.mjs +44 -0
  79. package/src/server/data/catalog.mjs +1091 -0
  80. package/src/server/data/entry-states.mjs +499 -0
  81. package/src/server/data/live2d.mjs +272 -0
  82. package/src/server/data/manifest-cache.mjs +58 -0
  83. package/src/server/data/manifest-stamp.mjs +135 -0
  84. package/src/server/data/model-meta.mjs +114 -0
  85. package/src/server/data/names.mjs +106 -0
  86. package/src/server/data/shape-probe-worker.mjs +33 -0
  87. package/src/server/data/shape-probe.mjs +626 -0
  88. package/src/server/data/spine.mjs +336 -0
  89. package/src/server/data/tree.mjs +126 -0
  90. package/src/server/http/atomic-json.mjs +145 -0
  91. package/src/server/http/background-solids.mjs +270 -0
  92. package/src/server/http/backgrounds.mjs +388 -0
  93. package/src/server/http/diag-upload.mjs +264 -0
  94. package/src/server/http/diag.mjs +173 -0
  95. package/src/server/http/ready.mjs +167 -0
  96. package/src/server/http/static.mjs +312 -0
  97. package/src/server/http/write.mjs +654 -0
  98. package/src/server/index.mjs +114 -0
  99. package/src/server/plugins/builtin-providers.json +30 -0
  100. package/src/server/plugins/registry.mjs +650 -0
  101. package/src/server/plugins/slot-catalog.mjs +286 -0
  102. package/src/server/routes/background-solids.mjs +56 -0
  103. package/src/server/routes/backgrounds.mjs +77 -0
  104. package/src/server/routes/config.mjs +38 -0
  105. package/src/server/routes/data.mjs +313 -0
  106. package/src/server/routes/diag.mjs +37 -0
  107. package/src/server/routes/entries.mjs +170 -0
  108. package/src/server/routes/ready.mjs +122 -0
  109. package/src/server/serve.mjs +396 -0
  110. package/src/server/unpack/capacity.mjs +217 -0
  111. package/src/server/unpack/mem-probe.mjs +27 -0
  112. package/src/server/unpack/plugin-runner.mjs +529 -0
  113. package/src/server/unpack/plugin-store.mjs +736 -0
  114. package/src/server/unpack/probe-store.mjs +73 -0
  115. package/src/server/unpack/routes.mjs +359 -0
  116. package/src/server/unpack/runtime/exec.mjs +558 -0
  117. package/src/server/unpack/runtime/index.mjs +141 -0
  118. package/src/server/unpack/runtime/node-vm.mjs +111 -0
  119. package/src/server/unpack/unpack-worker.mjs +105 -0
  120. package/src/server/unpack/worker-client.mjs +184 -0
  121. package/src/server/unpack/write-result.mjs +84 -0
  122. package/src/shared/bundle-shape.mjs +83 -0
  123. package/src/shared/entry-reasons.mjs +119 -0
  124. package/src/shared/names.mjs +45 -0
  125. package/src/shared/type-labels.mjs +31 -0
  126. package/src/web/README.md +193 -0
  127. package/src/web/app/anim-list.js +222 -0
  128. package/src/web/app/background-solids.js +150 -0
  129. package/src/web/app/backgrounds.js +152 -0
  130. package/src/web/app/camera-log.js +28 -0
  131. package/src/web/app/context.js +72 -0
  132. package/src/web/app/dir-picker.js +242 -0
  133. package/src/web/app/entry-report.js +71 -0
  134. package/src/web/app/memory-bars.js +91 -0
  135. package/src/web/app/memory-budget.js +228 -0
  136. package/src/web/app/memory-guard.js +204 -0
  137. package/src/web/app/model-actions.js +682 -0
  138. package/src/web/app/renderer-setting.js +70 -0
  139. package/src/web/app/root-manager.js +254 -0
  140. package/src/web/app/status.js +138 -0
  141. package/src/web/app/topbar-actions.js +357 -0
  142. package/src/web/app/transport-actions.js +297 -0
  143. package/src/web/app/ui-state.js +559 -0
  144. package/src/web/app/url-params.js +171 -0
  145. package/src/web/app/view-settings.js +186 -0
  146. package/src/web/components.css +524 -0
  147. package/src/web/debug/diag-hud.js +95 -0
  148. package/src/web/debug/drag.js +369 -0
  149. package/src/web/debug/log.js +320 -0
  150. package/src/web/debug/model-cmp.js +292 -0
  151. package/src/web/debug/panel.css +474 -0
  152. package/src/web/debug/panel.js +712 -0
  153. package/src/web/debug/race-probe.js +82 -0
  154. package/src/web/debug/upload.js +130 -0
  155. package/src/web/engine/adapters/atlas.js +47 -0
  156. package/src/web/engine/adapters/index.js +22 -0
  157. package/src/web/engine/adapters/renderer.js +223 -0
  158. package/src/web/engine/adapters/skeleton.js +105 -0
  159. package/src/web/engine/adapters/texture.js +64 -0
  160. package/src/web/engine/animation.js +181 -0
  161. package/src/web/engine/appearance.js +136 -0
  162. package/src/web/engine/camera-adapter.js +212 -0
  163. package/src/web/engine/effect-chain.js +138 -0
  164. package/src/web/engine/engine-state.js +120 -0
  165. package/src/web/engine/filters.js +224 -0
  166. package/src/web/engine/fit.js +197 -0
  167. package/src/web/engine/frame-gate.js +33 -0
  168. package/src/web/engine/gl-driver.js +56 -0
  169. package/src/web/engine/gl.js +77 -0
  170. package/src/web/engine/index.js +20 -0
  171. package/src/web/engine/loader.js +55 -0
  172. package/src/web/engine/model-info.js +26 -0
  173. package/src/web/engine/pipeline.js +200 -0
  174. package/src/web/engine/player.js +427 -0
  175. package/src/web/engine/pma/blend.js +76 -0
  176. package/src/web/engine/pma/detect.js +118 -0
  177. package/src/web/engine/pma/index.js +8 -0
  178. package/src/web/engine/pma/resolve.js +112 -0
  179. package/src/web/engine/pma/upload.js +127 -0
  180. package/src/web/engine/pma.js +5 -0
  181. package/src/web/engine/render-frame.js +93 -0
  182. package/src/web/engine/runtime-capabilities.js +209 -0
  183. package/src/web/engine/runtime.js +37 -0
  184. package/src/web/engine/skins.js +165 -0
  185. package/src/web/engine/textures.js +174 -0
  186. package/src/web/index.html +117 -0
  187. package/src/web/infra/api.js +531 -0
  188. package/src/web/infra/diag.js +509 -0
  189. package/src/web/infra/document.js +269 -0
  190. package/src/web/infra/dom.js +31 -0
  191. package/src/web/infra/events.mjs +27 -0
  192. package/src/web/infra/log.js +153 -0
  193. package/src/web/infra/mem-stats.js +112 -0
  194. package/src/web/infra/plugin-host-registration.mjs +285 -0
  195. package/src/web/infra/plugin-host-report.mjs +32 -0
  196. package/src/web/infra/plugin-host-unload.mjs +79 -0
  197. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  198. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  199. package/src/web/infra/plugin-host.js +628 -0
  200. package/src/web/infra/ready.js +461 -0
  201. package/src/web/infra/schedule.js +51 -0
  202. package/src/web/infra/shortcuts.js +129 -0
  203. package/src/web/infra/slot-registry.js +402 -0
  204. package/src/web/infra/vfs.js +247 -0
  205. package/src/web/live2d/bridge.js +400 -0
  206. package/src/web/live2d/model.js +321 -0
  207. package/src/web/live2d/overlay.js +142 -0
  208. package/src/web/live2d/pixi-app.js +111 -0
  209. package/src/web/live2d/playback-probe.js +92 -0
  210. package/src/web/live2d/probe-geometry.js +329 -0
  211. package/src/web/live2d/probe.js +119 -0
  212. package/src/web/live2d/release.js +66 -0
  213. package/src/web/live2d/runtime-adapter.js +289 -0
  214. package/src/web/live2d/stage-ctx.js +152 -0
  215. package/src/web/live2d/stage.js +204 -0
  216. package/src/web/live2d/vfs-settings.js +174 -0
  217. package/src/web/live2d/view.js +89 -0
  218. package/src/web/main.js +629 -0
  219. package/src/web/panel/background-list.js +373 -0
  220. package/src/web/panel/bg-source.js +291 -0
  221. package/src/web/panel/color-picker.css +199 -0
  222. package/src/web/panel/color-picker.js +278 -0
  223. package/src/web/panel/dialog.css +116 -0
  224. package/src/web/panel/dialog.js +173 -0
  225. package/src/web/panel/dom.js +35 -0
  226. package/src/web/panel/icons.js +58 -0
  227. package/src/web/panel/memory-bar.js +195 -0
  228. package/src/web/panel/panel-settings.js +1263 -0
  229. package/src/web/panel/panel.css +1304 -0
  230. package/src/web/panel/render-choice.js +104 -0
  231. package/src/web/panel/schema.js +784 -0
  232. package/src/web/panel/settings-io.js +291 -0
  233. package/src/web/panel/shell-controls.js +274 -0
  234. package/src/web/panel/slider.js +210 -0
  235. package/src/web/panel/toolbar-schema.js +111 -0
  236. package/src/web/panel/toolbar.js +195 -0
  237. package/src/web/panel/transport-picker.js +375 -0
  238. package/src/web/panel/transport.js +321 -0
  239. package/src/web/panel/view.js +553 -0
  240. package/src/web/plugin/unpack-worker.js +185 -0
  241. package/src/web/render/camera/constants.js +60 -0
  242. package/src/web/render/camera/index.js +123 -0
  243. package/src/web/render/camera/spine.js +124 -0
  244. package/src/web/render/capture.js +30 -0
  245. package/src/web/render/interaction.js +527 -0
  246. package/src/web/render/live2d.js +627 -0
  247. package/src/web/render/playback.js +47 -0
  248. package/src/web/render/reset-view.js +29 -0
  249. package/src/web/render/spine.js +386 -0
  250. package/src/web/render/surface-switch.js +176 -0
  251. package/src/web/render/surface.js +571 -0
  252. package/src/web/render/surfaces.js +70 -0
  253. package/src/web/roster/filter.js +31 -0
  254. package/src/web/roster/list.js +276 -0
  255. package/src/web/roster/persist.js +92 -0
  256. package/src/web/roster/reveal.js +77 -0
  257. package/src/web/roster/rows.js +248 -0
  258. package/src/web/roster/snapshot.js +39 -0
  259. package/src/web/roster/type-filter.js +97 -0
  260. package/src/web/roster/view.js +761 -0
  261. package/src/web/spine/canvas-api-groups.js +267 -0
  262. package/src/web/spine/canvas-api.js +97 -0
  263. package/src/web/spine/canvas-document.js +80 -0
  264. package/src/web/spine/canvas-engine.js +193 -0
  265. package/src/web/spine/canvas-entry.js +28 -0
  266. package/src/web/spine/canvas-pixels.js +36 -0
  267. package/src/web/spine/canvas-runtime.js +127 -0
  268. package/src/web/spine/canvas-view.js +108 -0
  269. package/src/web/spine/canvas.js +7 -0
  270. package/src/web/spine/runtime-hint.js +73 -0
  271. package/src/web/spine/runtimes.js +212 -0
  272. package/src/web/style.css +493 -0
  273. package/tool/lib/bundle-probe-node.mjs +121 -0
  274. package/tool/lib/paths.mjs +20 -0
  275. package/tool/lib/runtime-guides.mjs +65 -0
  276. package/tool/lib/runtime-package.mjs +403 -0
  277. package/tool/lib/spine-meta.mjs +227 -0
  278. package/tool/lib/spine-motions.mjs +104 -0
  279. package/tool/parse-jsonc.mjs +86 -0
  280. package/tool/resolve-live2d-runtimes.mjs +276 -0
  281. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,199 @@
1
+ /* panel/color-picker.css —— 自研调色盘的**全部**样式(组件内不写任何内联 style)
2
+ *
3
+ * 结构:
4
+ * .cp-mask 全屏遮罩(点击 = 取消)
5
+ * .cp-panel 面板本体(可被锚点定位)
6
+ * .cp-preview 颜色预览块(背景由 --cp-preview 驱动)
7
+ * .cp-rows 三行滑块(色相 / 饱和度 / 亮度)
8
+ * .cp-hex #rrggbb 输入框(非法时 .is-bad)
9
+ * .cp-actions 取消 / 确定
10
+ *
11
+ * 渐变轨道为什么用 CSS 变量:
12
+ * 色相条是固定渐变(红→黄→绿→青→蓝→洋红)⇒ 直接写在 CSS 里;
13
+ * 饱和度/亮度条的端点色随当前颜色变化 ⇒ 由 JS 只设 --cp-sat-from / --cp-sat-to /
14
+ * --cp-lit-from / --cp-lit-to 四个变量,渐变本身仍在 CSS 里声明。
15
+ * 这样"颜色值怎么变成渐变"这件事只有一处(这里),JS 不拼 background 字面量。
16
+ */
17
+
18
+ .cp-mask {
19
+ position: fixed;
20
+ inset: 0;
21
+ z-index: 9000;
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ background: rgba(0, 0, 0, 0.45);
26
+ }
27
+
28
+ .cp-panel {
29
+ /* 定位:默认居中(遮罩是 flex 居中);给了 anchor 时由 JS 写 left/top 并加 .is-anchored */
30
+ position: relative;
31
+ width: 268px;
32
+ padding: 12px;
33
+ border-radius: 10px;
34
+ border: 1px solid var(--color-border, #3a3f45);
35
+ background: var(--color-card, #23272b);
36
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
37
+ color: var(--color-fg, #e6e8ea);
38
+ font-size: 12px;
39
+ box-sizing: border-box;
40
+ }
41
+
42
+ .cp-panel.is-anchored {
43
+ position: fixed;
44
+ margin: 0;
45
+ }
46
+
47
+ .cp-top {
48
+ display: flex;
49
+ gap: 10px;
50
+ align-items: stretch;
51
+ }
52
+
53
+ .cp-preview {
54
+ width: 62px;
55
+ flex: none;
56
+ border-radius: 8px;
57
+ border: 1px solid rgba(255, 255, 255, 0.16);
58
+ /* 预览色只由变量决定 */
59
+ background: var(--cp-preview, #2a2a2a);
60
+ /* 棋盘格打底:半透明色也能看出是"有颜色" */
61
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
62
+ }
63
+
64
+ .cp-rows {
65
+ flex: 1 1 auto;
66
+ min-width: 0;
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 6px;
70
+ }
71
+
72
+ .cp-row {
73
+ display: grid;
74
+ grid-template-columns: 14px 1fr;
75
+ align-items: center;
76
+ gap: 6px;
77
+ }
78
+
79
+ .cp-row-label {
80
+ font-size: 11px;
81
+ opacity: 0.75;
82
+ text-align: center;
83
+ }
84
+
85
+ /* 三条轨道共用外观,只有 background 不同 */
86
+ .cp-slider {
87
+ -webkit-appearance: none;
88
+ appearance: none;
89
+ width: 100%;
90
+ height: 12px;
91
+ border-radius: 6px;
92
+ border: 1px solid rgba(255, 255, 255, 0.14);
93
+ outline: none;
94
+ cursor: pointer;
95
+ }
96
+
97
+ .cp-slider::-webkit-slider-thumb {
98
+ -webkit-appearance: none;
99
+ appearance: none;
100
+ width: 12px;
101
+ height: 12px;
102
+ border-radius: 50%;
103
+ border: 2px solid #fff;
104
+ background: transparent;
105
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
106
+ }
107
+
108
+ .cp-slider::-moz-range-thumb {
109
+ width: 12px;
110
+ height: 12px;
111
+ border-radius: 50%;
112
+ border: 2px solid #fff;
113
+ background: transparent;
114
+ }
115
+
116
+ /* 色相:固定渐变(0°红 → 60°黄 → 120°绿 → 180°青 → 240°蓝 → 300°洋红 → 360°红) */
117
+ .cp-slider--h {
118
+ background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%);
119
+ }
120
+
121
+ /* 饱和度:从"同亮度下的灰"到"当前色相的纯色",端点色由 JS 给变量 */
122
+ .cp-slider--s {
123
+ background: linear-gradient(to right, var(--cp-sat-from, #808080), var(--cp-sat-to, #f00));
124
+ }
125
+
126
+ /* 亮度:黑 → 当前色 → 白 */
127
+ .cp-slider--l {
128
+ background: linear-gradient(to right, #000, var(--cp-lit-mid, #f00) 50%, #fff);
129
+ }
130
+
131
+ .cp-hex-row {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 6px;
135
+ margin-top: 10px;
136
+ }
137
+
138
+ .cp-hex-label {
139
+ flex: none;
140
+ font-size: 11px;
141
+ opacity: 0.75;
142
+ }
143
+
144
+ .cp-hex {
145
+ flex: 1 1 auto;
146
+ min-width: 0;
147
+ height: 26px;
148
+ padding: 0 8px;
149
+ border-radius: 6px;
150
+ border: 1px solid var(--color-border, #3a3f45);
151
+ background: var(--color-bg, #1b1e21);
152
+ color: var(--color-fg, #e6e8ea);
153
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
154
+ font-size: 12px;
155
+ box-sizing: border-box;
156
+ }
157
+
158
+ .cp-hex:focus {
159
+ outline: 1px solid var(--color-primary, #1fc0c5);
160
+ border-color: var(--color-primary, #1fc0c5);
161
+ }
162
+
163
+ /* 非法:#rrggbb 之外的内容 —— 输入框标红 + 确定禁用 */
164
+ .cp-hex.is-bad {
165
+ border-color: #e05252;
166
+ color: #ffb4b4;
167
+ }
168
+
169
+ .cp-actions {
170
+ display: flex;
171
+ justify-content: flex-end;
172
+ gap: 8px;
173
+ margin-top: 10px;
174
+ }
175
+
176
+ .cp-btn {
177
+ height: 26px;
178
+ padding: 0 12px;
179
+ border-radius: 6px;
180
+ border: 1px solid var(--color-border, #3a3f45);
181
+ background: var(--color-secondary, #2d3237);
182
+ color: var(--color-fg, #e6e8ea);
183
+ font-size: 12px;
184
+ cursor: pointer;
185
+ }
186
+
187
+ .cp-btn:hover {
188
+ border-color: var(--color-primary, #1fc0c5);
189
+ }
190
+
191
+ .cp-btn--primary {
192
+ border-color: var(--color-primary, #1fc0c5);
193
+ color: var(--color-primary, #1fc0c5);
194
+ }
195
+
196
+ .cp-btn[disabled] {
197
+ opacity: 0.45;
198
+ cursor: not-allowed;
199
+ }
@@ -0,0 +1,278 @@
1
+ // panel/color-picker.js —— 自研调色盘(**纯 UI 组件,无状态**)
2
+ //
3
+ // 职责边界(硬约束):
4
+ // 只做:弹出调色盘 → 用户选完 → 回调返回一个 '#rrggbb' 字符串。
5
+ // 不做:不碰本地存储、不调任何接口、不认识"预设"这个概念、不负责命名、不缓存上次选择。
6
+ // 自证方式:`openColorPicker` 的函数体里只做「取值 → 校色 → 回调」,没有任何外部状态读写。
7
+ //
8
+ // 为什么自己写:项目零依赖(不引第三方取色器),而原生 <input type="color"> 在无头环境与
9
+ // 部分浏览器下弹的是系统对话框,样式不可控、也无法做"十六进制输入 + 三滑块双向同步"。
10
+ //
11
+ // 交互口径:
12
+ // · HSL 三滑块驱动预览;hex 输入双向同步(合法即回写滑块,非法则标红 + 禁用确定)
13
+ // · Esc / 点遮罩 / 取消 ⇒ onCancel(不回调 onConfirm)
14
+ // · 单例:重复 open 会先销毁前一个,避免两个遮罩叠在一起
15
+
16
+ import { el } from './dom.js';
17
+
18
+ /** 样式表只注入一次(面板会重建,重复插入 <link> 会浪费) */
19
+ const CSS_HREF = 'panel/color-picker.css';
20
+ const CSS_ATTR = 'data-color-picker-css';
21
+
22
+ /** 当前打开的实例(单例约束) */
23
+ let current = null;
24
+
25
+ /** 严格校验 6 位十六进制(大小写都收,输出统一小写) */
26
+ const HEX_RE = /^#[0-9a-fA-F]{6}$/;
27
+
28
+ /**
29
+ * 规范化颜色:合法则返回小写 '#rrggbb',否则返回 null。
30
+ * @param {string} v
31
+ * @returns {string|null}
32
+ */
33
+ function normalizeHex(v) {
34
+ const s = String(v || '').trim();
35
+ return HEX_RE.test(s) ? s.toLowerCase() : null;
36
+ }
37
+
38
+ /**
39
+ * #rrggbb → {h,s,l}(h:0-360, s/l:0-100)。
40
+ * @param {string} hex 已规范化的颜色
41
+ * @returns {{h:number,s:number,l:number}}
42
+ */
43
+ function hexToHsl(hex) {
44
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
45
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
46
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
47
+ const max = Math.max(r, g, b);
48
+ const min = Math.min(r, g, b);
49
+ const d = max - min;
50
+ const l = (max + min) / 2;
51
+ let h = 0;
52
+ let s = 0;
53
+ if (d !== 0) {
54
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
55
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
56
+ else if (max === g) h = ((b - r) / d + 2) / 6;
57
+ else h = ((r - g) / d + 4) / 6;
58
+ }
59
+ // 不取整:取整会在「输入 hex → 转 HSL → 再转回 hex」时丢精度(实测 #8844cc 会变成 #8743cb)
60
+ return { h: h * 360, s: s * 100, l: l * 100 };
61
+ }
62
+
63
+ /**
64
+ * {h,s,l} → '#rrggbb'。
65
+ * @param {number} h 0-360
66
+ * @param {number} s 0-100
67
+ * @param {number} l 0-100
68
+ * @returns {string}
69
+ */
70
+ function hslToHex(h, s, l) {
71
+ const hh = ((h % 360) + 360) % 360 / 360;
72
+ const ss = Math.min(100, Math.max(0, s)) / 100;
73
+ const ll = Math.min(100, Math.max(0, l)) / 100;
74
+ const f = (n) => {
75
+ const k = (n + hh * 12) % 12;
76
+ const a = ss * Math.min(ll, 1 - ll);
77
+ const v = ll - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
78
+ return Math.round(255 * v).toString(16).padStart(2, '0');
79
+ };
80
+ return `#${f(0)}${f(8)}${f(4)}`;
81
+ }
82
+
83
+ /** 注入样式表(幂等;失败不阻塞组件可用) */
84
+ function ensureStylesheet(doc) {
85
+ if (doc.querySelector(`link[${CSS_ATTR}]`)) return;
86
+ const link = doc.createElement('link');
87
+ link.rel = 'stylesheet';
88
+ link.href = CSS_HREF;
89
+ link.setAttribute(CSS_ATTR, '1');
90
+ (doc.head || doc.documentElement).append(link);
91
+ }
92
+
93
+ /**
94
+ * 打开调色盘。
95
+ *
96
+ * @param {object} opts
97
+ * @param {string} [opts.initial] 初始颜色 '#rrggbb'(非法/缺省时用 #000000)
98
+ * @param {HTMLElement} [opts.anchor] 可选锚点(就近弹出;缺省居中)
99
+ * @param {(color:string)=>void} opts.onConfirm 用户点确定时回调,参数是规范化后的 '#rrggbb'
100
+ * @param {()=>void} [opts.onCancel] 取消/Esc/点遮罩时回调
101
+ * @returns {{destroy: () => void}} 句柄(重复 open 会先销毁上一个)
102
+ */
103
+ export function openColorPicker(opts = {}) {
104
+ if (current) { try { current.destroy(); } catch { /* 旧实例销毁失败不影响新开 */ } }
105
+
106
+ const doc = (opts.anchor && opts.anchor.ownerDocument) || document;
107
+ ensureStylesheet(doc);
108
+
109
+ const start = hexToHsl(normalizeHex(opts.initial) || '#000000');
110
+ let hue = start.h;
111
+ let sat = start.s;
112
+ let lit = start.l;
113
+ let hex = hslToHex(hue, sat, lit);
114
+ /** 用户直接输入且合法的原始值(滑块没动过就原样返回,避免 HSL 往返丢精度) */
115
+ let exactHex = null;
116
+ let closed = false;
117
+
118
+ const mask = el('div', 'cp-mask');
119
+ const panel = el('div', 'cp-panel');
120
+ panel.setAttribute('role', 'dialog');
121
+ panel.setAttribute('aria-label', '选择颜色');
122
+
123
+ const preview = el('div', 'cp-preview');
124
+ const rows = el('div', 'cp-rows');
125
+
126
+ /** 造一行滑块(标签 + input[type=range]) */
127
+ const makeRow = (label, cls, max, value) => {
128
+ const row = el('div', 'cp-row');
129
+ row.append(el('span', 'cp-row-label', label));
130
+ const input = el('input', `cp-slider ${cls}`);
131
+ input.type = 'range';
132
+ input.min = '0';
133
+ input.max = String(max);
134
+ input.step = '1';
135
+ input.value = String(value);
136
+ input.setAttribute('aria-label', { H: '色相', S: '饱和度', L: '亮度' }[label] || label);
137
+ row.append(input);
138
+ rows.append(row);
139
+ return input;
140
+ };
141
+
142
+ const hIn = makeRow('H', 'cp-slider--h', 360, hue);
143
+ const sIn = makeRow('S', 'cp-slider--s', 100, sat);
144
+ const lIn = makeRow('L', 'cp-slider--l', 100, lit);
145
+
146
+ const top = el('div', 'cp-top');
147
+ top.append(preview, rows);
148
+
149
+ const hexRow = el('div', 'cp-hex-row');
150
+ hexRow.append(el('span', 'cp-hex-label', '十六进制'));
151
+ const hexIn = el('input', 'cp-hex');
152
+ hexIn.type = 'text';
153
+ hexIn.value = hex;
154
+ hexIn.spellcheck = false;
155
+ hexIn.setAttribute('aria-label', '十六进制颜色');
156
+ hexRow.append(hexIn);
157
+
158
+ const actions = el('div', 'cp-actions');
159
+ const btnCancel = el('button', 'cp-btn', '取消');
160
+ btnCancel.type = 'button';
161
+ const btnOk = el('button', 'cp-btn cp-btn--primary', '确定');
162
+ btnOk.type = 'button';
163
+ actions.append(btnCancel, btnOk);
164
+
165
+ panel.append(top, hexRow, actions);
166
+ mask.append(panel);
167
+ (doc.body || doc.documentElement).append(mask);
168
+
169
+ /**
170
+ * 把当前 h/s/l 写进 DOM(预览、滑块、变量、hex)。
171
+ * 只设 CSS 变量与 input.value —— 不拼 background 字面量。
172
+ */
173
+ const paint = () => {
174
+ hex = exactHex || hslToHex(hue, sat, lit);
175
+ preview.style.setProperty('--cp-preview', hex);
176
+ // 饱和度条两端:左端同亮度灰、右端当前色相纯色
177
+ panel.style.setProperty('--cp-sat-from', hslToHex(hue, 0, lit));
178
+ panel.style.setProperty('--cp-sat-to', hslToHex(hue, 100, lit));
179
+ // 亮度条中点:当前色相/饱和度的"标准亮度"
180
+ panel.style.setProperty('--cp-lit-mid', hslToHex(hue, sat, 50));
181
+ hIn.value = String(hue);
182
+ sIn.value = String(sat);
183
+ lIn.value = String(lit);
184
+ hexIn.value = hex;
185
+ hexIn.classList.remove('is-bad');
186
+ btnOk.disabled = false;
187
+ };
188
+
189
+ /** 滑块 → 状态 → 重绘 */
190
+ const onSlide = () => {
191
+ hue = Number(hIn.value);
192
+ sat = Number(sIn.value);
193
+ lit = Number(lIn.value);
194
+ exactHex = null; // 滑块一动就按 HSL 算,原始输入值不再代表当前色
195
+ paint();
196
+ };
197
+
198
+ /** hex 输入 → 合法则回写滑块;非法标红并禁用确定 */
199
+ const onHexInput = () => {
200
+ const ok = normalizeHex(hexIn.value);
201
+ if (!ok) {
202
+ hexIn.classList.add('is-bad');
203
+ btnOk.disabled = true;
204
+ return;
205
+ }
206
+ const hsl = hexToHsl(ok);
207
+ hue = hsl.h; sat = hsl.s; lit = hsl.l;
208
+ exactHex = ok; // 原样保留用户输入的精确值
209
+ paint();
210
+ };
211
+
212
+ hIn.addEventListener('input', onSlide);
213
+ sIn.addEventListener('input', onSlide);
214
+ lIn.addEventListener('input', onSlide);
215
+ hexIn.addEventListener('input', onHexInput);
216
+
217
+ /** 关闭并清理(先标记 closed,避免 Esc 与遮罩点击重复回调) */
218
+ const close = (confirmed) => {
219
+ if (closed) return;
220
+ closed = true;
221
+ doc.removeEventListener('keydown', onKey, true);
222
+ try { mask.remove(); } catch { /* 已被移除 */ }
223
+ current = null;
224
+ if (confirmed) {
225
+ const v = normalizeHex(hexIn.value) || hex;
226
+ if (typeof opts.onConfirm === 'function') opts.onConfirm(v);
227
+ } else if (typeof opts.onCancel === 'function') opts.onCancel();
228
+ };
229
+
230
+ /** Esc 只取消(捕获阶段,避免被页面其它 Esc 处理抢先) */
231
+ function onKey(e) {
232
+ if (e.key === 'Escape') { e.stopPropagation(); close(false); }
233
+ }
234
+ doc.addEventListener('keydown', onKey, true);
235
+
236
+ btnCancel.addEventListener('click', () => close(false));
237
+ btnOk.addEventListener('click', () => { if (!btnOk.disabled) close(true); });
238
+ mask.addEventListener('click', (e) => { if (e.target === mask) close(false); });
239
+
240
+ // 就近弹出:贴着锚点下方;越界时翻到上方/左移,保证整块在视口内
241
+ if (opts.anchor && typeof opts.anchor.getBoundingClientRect === 'function') {
242
+ /**
243
+ * 定位一次。
244
+ * 为什么要在 rAF 里再跑一次:刚 append 的面板还没布局,getBoundingClientRect 尺寸为 0,
245
+ * 拿 0 尺寸判越界会把坐标错误地夹到视口边缘(实测跑到左下角 8px)。
246
+ */
247
+ const place = () => {
248
+ const r = opts.anchor.getBoundingClientRect();
249
+ const box = panel.getBoundingClientRect();
250
+ const vw = window.innerWidth || 0;
251
+ const vh = window.innerHeight || 0;
252
+ let left = r.left;
253
+ let top = r.bottom + 6;
254
+ if (vw && box.width && left + box.width > vw - 8) left = Math.max(8, vw - box.width - 8);
255
+ if (vh && box.height && top + box.height > vh - 8) top = Math.max(8, r.top - box.height - 6);
256
+ panel.classList.add('is-anchored');
257
+ // 坐标是运行时算出来的动态值(契约允许的唯一动态例外)
258
+ panel.style.left = `${Math.round(left)}px`;
259
+ panel.style.top = `${Math.round(top)}px`;
260
+ };
261
+ place();
262
+ requestAnimationFrame(place);
263
+ }
264
+
265
+ paint();
266
+
267
+ const handle = {
268
+ destroy() {
269
+ if (closed) return;
270
+ closed = true;
271
+ doc.removeEventListener('keydown', onKey, true);
272
+ try { mask.remove(); } catch { /* 已被移除 */ }
273
+ if (current === handle) current = null;
274
+ },
275
+ };
276
+ current = handle;
277
+ return handle;
278
+ }
@@ -0,0 +1,116 @@
1
+ /* panel/dialog.css —— 自绘弹窗的全部样式(组件内不写内联 style)
2
+ *
3
+ * 配色一律走主题变量(--color-*),这样夜间/日间主题切换时弹窗跟着变,
4
+ * 不会出现"深色面板 + 系统白底弹窗"的视觉断层。
5
+ * z-index 10000:必须高于现有最高层(调试日志浮层 9500),否则被浮层盖住。
6
+ */
7
+
8
+ .dlg-mask {
9
+ position: fixed;
10
+ inset: 0;
11
+ z-index: 10000;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ background: rgba(0, 0, 0, 0.55);
16
+ }
17
+
18
+ .dlg-panel {
19
+ width: 320px;
20
+ max-width: calc(100vw - 32px);
21
+ padding: 16px;
22
+ border-radius: 12px;
23
+ border: 1px solid var(--color-border, #3a3f45);
24
+ background: var(--color-card, #23272b);
25
+ color: var(--color-fg, #e6e8ea);
26
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ .dlg-title {
31
+ margin: 0 0 8px;
32
+ font-size: 14px;
33
+ font-weight: 600;
34
+ line-height: 1.4;
35
+ }
36
+
37
+ .dlg-message {
38
+ margin: 0 0 12px;
39
+ font-size: 12px;
40
+ line-height: 1.6;
41
+ /* 用前景色 + 降透明度:--color-muted 在深色底上对比太低(实拍几乎看不见) */
42
+ color: var(--color-fg, #e6e8ea);
43
+ opacity: 0.78;
44
+ /* 长名称(图片名可能很长)要能换行,不能撑破面板 */
45
+ word-break: break-word;
46
+ }
47
+
48
+ /* 输入行:输入框 + 右侧实时字数 */
49
+ .dlg-input-row {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 6px;
53
+ margin-bottom: 14px;
54
+ }
55
+
56
+ .dlg-input {
57
+ flex: 1 1 auto;
58
+ min-width: 0;
59
+ height: 36px;
60
+ padding: 0 10px;
61
+ border-radius: 8px;
62
+ border: 1px solid var(--color-border, #3a3f45);
63
+ background: var(--color-bg, #1b1e21);
64
+ color: var(--color-fg, #e6e8ea);
65
+ font-size: 14px;
66
+ box-sizing: border-box;
67
+ }
68
+
69
+ .dlg-input:focus {
70
+ outline: 1px solid var(--color-primary, #1fc0c5);
71
+ border-color: var(--color-primary, #1fc0c5);
72
+ }
73
+
74
+ .dlg-counter {
75
+ flex: 0 0 auto;
76
+ font-size: 11px;
77
+ color: var(--color-fg, #e6e8ea);
78
+ opacity: 0.7;
79
+ font-variant-numeric: tabular-nums;
80
+ white-space: nowrap;
81
+ }
82
+
83
+ .dlg-actions {
84
+ display: flex;
85
+ justify-content: flex-end;
86
+ gap: 8px;
87
+ }
88
+
89
+ .dlg-btn {
90
+ /* 44px:手机手指友好的最小高度 */
91
+ min-height: 44px;
92
+ min-width: 84px;
93
+ padding: 0 16px;
94
+ border-radius: 8px;
95
+ border: 1px solid var(--color-border, #3a3f45);
96
+ background: var(--color-secondary, #2d3237);
97
+ color: var(--color-fg, #e6e8ea);
98
+ font-size: 14px;
99
+ cursor: pointer;
100
+ }
101
+
102
+ .dlg-btn:hover { border-color: var(--color-primary, #1fc0c5); }
103
+
104
+ .dlg-btn--primary {
105
+ border-color: var(--color-primary, #1fc0c5);
106
+ color: var(--color-primary, #1fc0c5);
107
+ }
108
+
109
+ /* 危险操作的确定键:红色,与"删除"语义一致 */
110
+ .dlg-btn--danger {
111
+ border-color: #e05a5a;
112
+ color: #fff;
113
+ background: #c94444;
114
+ }
115
+
116
+ .dlg-btn--danger:hover { border-color: #ff7b7b; }