@things-factory/board-ai 10.0.0-beta.108

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 (329) hide show
  1. package/PLAN-styling-tools.md +1253 -0
  2. package/client/components/board-ai-chat.test.ts +149 -0
  3. package/client/components/board-ai-chat.ts +2574 -0
  4. package/client/components/chat-echo-dedup.test.ts +116 -0
  5. package/client/components/chat-echo-dedup.ts +89 -0
  6. package/client/components/chat-input-builder.ts +47 -0
  7. package/client/components/default-catalog-entries.test.ts +59 -0
  8. package/client/components/default-catalog-entries.ts +100 -0
  9. package/client/components/default-slash-templates.test.ts +55 -0
  10. package/client/components/default-slash-templates.ts +129 -0
  11. package/client/components/markdown.test.ts +518 -0
  12. package/client/components/markdown.ts +508 -0
  13. package/client/components/mention-popup-helpers.ts +433 -0
  14. package/client/components/mention-popup.test.ts +588 -0
  15. package/client/components/mention-popup.ts +238 -0
  16. package/client/index.ts +11 -0
  17. package/client/tsconfig.json +13 -0
  18. package/client/utils/board-edit-patch.ts +374 -0
  19. package/config/config.development.js +43 -0
  20. package/config/config.production.js +15 -0
  21. package/dist-client/client/components/board-ai-chat.d.ts +277 -0
  22. package/dist-client/client/components/board-ai-chat.js +2444 -0
  23. package/dist-client/client/components/board-ai-chat.js.map +1 -0
  24. package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
  25. package/dist-client/client/components/board-ai-chat.test.js +138 -0
  26. package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
  27. package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
  28. package/dist-client/client/components/chat-echo-dedup.js +58 -0
  29. package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
  30. package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
  31. package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
  32. package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
  33. package/dist-client/client/components/chat-input-builder.d.ts +39 -0
  34. package/dist-client/client/components/chat-input-builder.js +26 -0
  35. package/dist-client/client/components/chat-input-builder.js.map +1 -0
  36. package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
  37. package/dist-client/client/components/default-catalog-entries.js +86 -0
  38. package/dist-client/client/components/default-catalog-entries.js.map +1 -0
  39. package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
  40. package/dist-client/client/components/default-catalog-entries.test.js +52 -0
  41. package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
  42. package/dist-client/client/components/default-slash-templates.d.ts +17 -0
  43. package/dist-client/client/components/default-slash-templates.js +113 -0
  44. package/dist-client/client/components/default-slash-templates.js.map +1 -0
  45. package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
  46. package/dist-client/client/components/default-slash-templates.test.js +51 -0
  47. package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
  48. package/dist-client/client/components/markdown.d.ts +60 -0
  49. package/dist-client/client/components/markdown.js +422 -0
  50. package/dist-client/client/components/markdown.js.map +1 -0
  51. package/dist-client/client/components/markdown.test.d.ts +1 -0
  52. package/dist-client/client/components/markdown.test.js +418 -0
  53. package/dist-client/client/components/markdown.test.js.map +1 -0
  54. package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
  55. package/dist-client/client/components/mention-popup-helpers.js +330 -0
  56. package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
  57. package/dist-client/client/components/mention-popup.d.ts +35 -0
  58. package/dist-client/client/components/mention-popup.js +223 -0
  59. package/dist-client/client/components/mention-popup.js.map +1 -0
  60. package/dist-client/client/components/mention-popup.test.d.ts +1 -0
  61. package/dist-client/client/components/mention-popup.test.js +474 -0
  62. package/dist-client/client/components/mention-popup.test.js.map +1 -0
  63. package/dist-client/client/index.d.ts +11 -0
  64. package/dist-client/client/index.js +12 -0
  65. package/dist-client/client/index.js.map +1 -0
  66. package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
  67. package/dist-client/client/utils/board-edit-patch.js +284 -0
  68. package/dist-client/client/utils/board-edit-patch.js.map +1 -0
  69. package/dist-client/server/service/agentic-loop.d.ts +87 -0
  70. package/dist-client/server/service/agentic-loop.js +276 -0
  71. package/dist-client/server/service/agentic-loop.js.map +1 -0
  72. package/dist-client/server/service/assistant.d.ts +84 -0
  73. package/dist-client/server/service/assistant.js +2220 -0
  74. package/dist-client/server/service/assistant.js.map +1 -0
  75. package/dist-client/server/service/catalog-resolver.d.ts +18 -0
  76. package/dist-client/server/service/catalog-resolver.js +93 -0
  77. package/dist-client/server/service/catalog-resolver.js.map +1 -0
  78. package/dist-client/server/service/component-tree.d.ts +68 -0
  79. package/dist-client/server/service/component-tree.js +68 -0
  80. package/dist-client/server/service/component-tree.js.map +1 -0
  81. package/dist-client/server/service/mention-resolver.d.ts +96 -0
  82. package/dist-client/server/service/mention-resolver.js +172 -0
  83. package/dist-client/server/service/mention-resolver.js.map +1 -0
  84. package/dist-client/server/service/prefab-registry.d.ts +73 -0
  85. package/dist-client/server/service/prefab-registry.js +252 -0
  86. package/dist-client/server/service/prefab-registry.js.map +1 -0
  87. package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
  88. package/dist-client/server/service/styling/clear-tools.js +50 -0
  89. package/dist-client/server/service/styling/clear-tools.js.map +1 -0
  90. package/dist-client/server/service/styling/color-utils.d.ts +43 -0
  91. package/dist-client/server/service/styling/color-utils.js +205 -0
  92. package/dist-client/server/service/styling/color-utils.js.map +1 -0
  93. package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
  94. package/dist-client/server/service/styling/copy-tools.js +78 -0
  95. package/dist-client/server/service/styling/copy-tools.js.map +1 -0
  96. package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
  97. package/dist-client/server/service/styling/effect-tools.js +85 -0
  98. package/dist-client/server/service/styling/effect-tools.js.map +1 -0
  99. package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
  100. package/dist-client/server/service/styling/fill-tools.js +226 -0
  101. package/dist-client/server/service/styling/fill-tools.js.map +1 -0
  102. package/dist-client/server/service/styling/material-tools.d.ts +32 -0
  103. package/dist-client/server/service/styling/material-tools.js +88 -0
  104. package/dist-client/server/service/styling/material-tools.js.map +1 -0
  105. package/dist-client/server/service/styling/read-tools.d.ts +34 -0
  106. package/dist-client/server/service/styling/read-tools.js +42 -0
  107. package/dist-client/server/service/styling/read-tools.js.map +1 -0
  108. package/dist-client/server/service/styling/registry.d.ts +91 -0
  109. package/dist-client/server/service/styling/registry.js +345 -0
  110. package/dist-client/server/service/styling/registry.js.map +1 -0
  111. package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
  112. package/dist-client/server/service/styling/stroke-tools.js +94 -0
  113. package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
  114. package/dist-client/server/service/styling/text-tools.d.ts +32 -0
  115. package/dist-client/server/service/styling/text-tools.js +101 -0
  116. package/dist-client/server/service/styling/text-tools.js.map +1 -0
  117. package/dist-client/server/service/types.d.ts +265 -0
  118. package/dist-client/server/service/types.js +2 -0
  119. package/dist-client/server/service/types.js.map +1 -0
  120. package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
  121. package/dist-client/server/service/validation/board-model-schema.js +402 -0
  122. package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
  123. package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
  124. package/dist-client/server/service/validation/tool-validation.js +193 -0
  125. package/dist-client/server/service/validation/tool-validation.js.map +1 -0
  126. package/dist-client/tsconfig.tsbuildinfo +1 -0
  127. package/dist-server/index.d.ts +23 -0
  128. package/dist-server/index.js +27 -0
  129. package/dist-server/index.js.map +1 -0
  130. package/dist-server/service/agentic-loop.d.ts +87 -0
  131. package/dist-server/service/agentic-loop.js +280 -0
  132. package/dist-server/service/agentic-loop.js.map +1 -0
  133. package/dist-server/service/apply-patch.d.ts +47 -0
  134. package/dist-server/service/apply-patch.js +369 -0
  135. package/dist-server/service/apply-patch.js.map +1 -0
  136. package/dist-server/service/assistant.d.ts +84 -0
  137. package/dist-server/service/assistant.js +2237 -0
  138. package/dist-server/service/assistant.js.map +1 -0
  139. package/dist-server/service/board-ai-resolver.d.ts +54 -0
  140. package/dist-server/service/board-ai-resolver.js +409 -0
  141. package/dist-server/service/board-ai-resolver.js.map +1 -0
  142. package/dist-server/service/catalog-resolver.d.ts +18 -0
  143. package/dist-server/service/catalog-resolver.js +98 -0
  144. package/dist-server/service/catalog-resolver.js.map +1 -0
  145. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
  146. package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
  147. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
  148. package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
  149. package/dist-server/service/chat-message/chat-message-publish.js +27 -0
  150. package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
  151. package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
  152. package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
  153. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
  154. package/dist-server/service/chat-message/chat-message.d.ts +56 -0
  155. package/dist-server/service/chat-message/chat-message.js +149 -0
  156. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  157. package/dist-server/service/chat-message/index.d.ts +5 -0
  158. package/dist-server/service/chat-message/index.js +12 -0
  159. package/dist-server/service/chat-message/index.js.map +1 -0
  160. package/dist-server/service/chat-session/chat-session.d.ts +31 -0
  161. package/dist-server/service/chat-session/chat-session.js +131 -0
  162. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  163. package/dist-server/service/chat-session/index.d.ts +3 -0
  164. package/dist-server/service/chat-session/index.js +7 -0
  165. package/dist-server/service/chat-session/index.js.map +1 -0
  166. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
  167. package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
  168. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
  169. package/dist-server/service/chat-session-participant/index.d.ts +4 -0
  170. package/dist-server/service/chat-session-participant/index.js +10 -0
  171. package/dist-server/service/chat-session-participant/index.js.map +1 -0
  172. package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
  173. package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
  174. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
  175. package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
  176. package/dist-server/service/chat-session-participant/presence.js +22 -0
  177. package/dist-server/service/chat-session-participant/presence.js.map +1 -0
  178. package/dist-server/service/chat-session-resolver.d.ts +20 -0
  179. package/dist-server/service/chat-session-resolver.js +356 -0
  180. package/dist-server/service/chat-session-resolver.js.map +1 -0
  181. package/dist-server/service/component-tree.d.ts +68 -0
  182. package/dist-server/service/component-tree.js +73 -0
  183. package/dist-server/service/component-tree.js.map +1 -0
  184. package/dist-server/service/index.d.ts +20 -0
  185. package/dist-server/service/index.js +34 -0
  186. package/dist-server/service/index.js.map +1 -0
  187. package/dist-server/service/mention-marker.d.ts +78 -0
  188. package/dist-server/service/mention-marker.js +162 -0
  189. package/dist-server/service/mention-marker.js.map +1 -0
  190. package/dist-server/service/mention-resolver.d.ts +96 -0
  191. package/dist-server/service/mention-resolver.js +177 -0
  192. package/dist-server/service/mention-resolver.js.map +1 -0
  193. package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
  194. package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
  195. package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
  196. package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
  197. package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
  198. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
  199. package/dist-server/service/patch-entry/index.d.ts +4 -0
  200. package/dist-server/service/patch-entry/index.js +12 -0
  201. package/dist-server/service/patch-entry/index.js.map +1 -0
  202. package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
  203. package/dist-server/service/patch-entry/patch-entry.js +96 -0
  204. package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
  205. package/dist-server/service/prefab-registry.d.ts +73 -0
  206. package/dist-server/service/prefab-registry.js +258 -0
  207. package/dist-server/service/prefab-registry.js.map +1 -0
  208. package/dist-server/service/styling/clear-tools.d.ts +15 -0
  209. package/dist-server/service/styling/clear-tools.js +53 -0
  210. package/dist-server/service/styling/clear-tools.js.map +1 -0
  211. package/dist-server/service/styling/color-utils.d.ts +43 -0
  212. package/dist-server/service/styling/color-utils.js +210 -0
  213. package/dist-server/service/styling/color-utils.js.map +1 -0
  214. package/dist-server/service/styling/copy-tools.d.ts +27 -0
  215. package/dist-server/service/styling/copy-tools.js +81 -0
  216. package/dist-server/service/styling/copy-tools.js.map +1 -0
  217. package/dist-server/service/styling/effect-tools.d.ts +45 -0
  218. package/dist-server/service/styling/effect-tools.js +90 -0
  219. package/dist-server/service/styling/effect-tools.js.map +1 -0
  220. package/dist-server/service/styling/fill-tools.d.ts +60 -0
  221. package/dist-server/service/styling/fill-tools.js +229 -0
  222. package/dist-server/service/styling/fill-tools.js.map +1 -0
  223. package/dist-server/service/styling/material-tools.d.ts +32 -0
  224. package/dist-server/service/styling/material-tools.js +92 -0
  225. package/dist-server/service/styling/material-tools.js.map +1 -0
  226. package/dist-server/service/styling/read-tools.d.ts +34 -0
  227. package/dist-server/service/styling/read-tools.js +45 -0
  228. package/dist-server/service/styling/read-tools.js.map +1 -0
  229. package/dist-server/service/styling/registry.d.ts +91 -0
  230. package/dist-server/service/styling/registry.js +352 -0
  231. package/dist-server/service/styling/registry.js.map +1 -0
  232. package/dist-server/service/styling/stroke-tools.d.ts +35 -0
  233. package/dist-server/service/styling/stroke-tools.js +97 -0
  234. package/dist-server/service/styling/stroke-tools.js.map +1 -0
  235. package/dist-server/service/styling/text-tools.d.ts +32 -0
  236. package/dist-server/service/styling/text-tools.js +105 -0
  237. package/dist-server/service/styling/text-tools.js.map +1 -0
  238. package/dist-server/service/subscribe-auth.d.ts +14 -0
  239. package/dist-server/service/subscribe-auth.js +33 -0
  240. package/dist-server/service/subscribe-auth.js.map +1 -0
  241. package/dist-server/service/types.d.ts +265 -0
  242. package/dist-server/service/types.js +3 -0
  243. package/dist-server/service/types.js.map +1 -0
  244. package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
  245. package/dist-server/service/validation/board-model-schema.js +409 -0
  246. package/dist-server/service/validation/board-model-schema.js.map +1 -0
  247. package/dist-server/service/validation/tool-validation.d.ts +31 -0
  248. package/dist-server/service/validation/tool-validation.js +196 -0
  249. package/dist-server/service/validation/tool-validation.js.map +1 -0
  250. package/dist-server/tsconfig.tsbuildinfo +1 -0
  251. package/package.json +48 -0
  252. package/server/index.ts +23 -0
  253. package/server/service/agentic-loop.test.ts +705 -0
  254. package/server/service/agentic-loop.ts +431 -0
  255. package/server/service/apply-patch-drift.test.ts +829 -0
  256. package/server/service/apply-patch.test.ts +757 -0
  257. package/server/service/apply-patch.ts +411 -0
  258. package/server/service/assistant-integration.test.ts +922 -0
  259. package/server/service/assistant-llm-smoke.test.ts +296 -0
  260. package/server/service/assistant.test.ts +2034 -0
  261. package/server/service/assistant.ts +2378 -0
  262. package/server/service/board-ai-resolver.ts +407 -0
  263. package/server/service/catalog-resolver.test.ts +137 -0
  264. package/server/service/catalog-resolver.ts +102 -0
  265. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
  266. package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
  267. package/server/service/chat-message/chat-message-broadcast.ts +27 -0
  268. package/server/service/chat-message/chat-message-publish.ts +22 -0
  269. package/server/service/chat-message/chat-message-subscription.ts +41 -0
  270. package/server/service/chat-message/chat-message.ts +169 -0
  271. package/server/service/chat-message/index.ts +12 -0
  272. package/server/service/chat-session/chat-session.ts +130 -0
  273. package/server/service/chat-session/index.ts +5 -0
  274. package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
  275. package/server/service/chat-session-participant/index.ts +6 -0
  276. package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
  277. package/server/service/chat-session-participant/invite-notification.ts +48 -0
  278. package/server/service/chat-session-participant/presence.test.ts +39 -0
  279. package/server/service/chat-session-participant/presence.ts +21 -0
  280. package/server/service/chat-session-resolver.ts +312 -0
  281. package/server/service/component-tree.test.ts +248 -0
  282. package/server/service/component-tree.ts +135 -0
  283. package/server/service/index.ts +32 -0
  284. package/server/service/mention-marker.extract.test.ts +61 -0
  285. package/server/service/mention-marker.test.ts +228 -0
  286. package/server/service/mention-marker.ts +184 -0
  287. package/server/service/mention-resolver.test.ts +273 -0
  288. package/server/service/mention-resolver.ts +264 -0
  289. package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
  290. package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
  291. package/server/service/patch-entry/board-patch-subscription.ts +50 -0
  292. package/server/service/patch-entry/index.ts +12 -0
  293. package/server/service/patch-entry/patch-entry.ts +89 -0
  294. package/server/service/prefab-registry.test.ts +172 -0
  295. package/server/service/prefab-registry.ts +332 -0
  296. package/server/service/styling/clear-tools.test.ts +164 -0
  297. package/server/service/styling/clear-tools.ts +69 -0
  298. package/server/service/styling/color-utils.test.ts +130 -0
  299. package/server/service/styling/color-utils.ts +226 -0
  300. package/server/service/styling/copy-tools.test.ts +152 -0
  301. package/server/service/styling/copy-tools.ts +115 -0
  302. package/server/service/styling/effect-tools.test.ts +277 -0
  303. package/server/service/styling/effect-tools.ts +124 -0
  304. package/server/service/styling/fill-tools.test.ts +331 -0
  305. package/server/service/styling/fill-tools.ts +293 -0
  306. package/server/service/styling/material-tools.test.ts +294 -0
  307. package/server/service/styling/material-tools.ts +114 -0
  308. package/server/service/styling/read-tools.test.ts +78 -0
  309. package/server/service/styling/read-tools.ts +67 -0
  310. package/server/service/styling/registry.test.ts +203 -0
  311. package/server/service/styling/registry.ts +423 -0
  312. package/server/service/styling/stroke-tools.test.ts +245 -0
  313. package/server/service/styling/stroke-tools.ts +145 -0
  314. package/server/service/styling/text-tools.test.ts +253 -0
  315. package/server/service/styling/text-tools.ts +131 -0
  316. package/server/service/styling/tier1-tools.test.ts +300 -0
  317. package/server/service/subscribe-auth.test.ts +57 -0
  318. package/server/service/subscribe-auth.ts +34 -0
  319. package/server/service/types.ts +235 -0
  320. package/server/service/validation/board-model-schema.test.ts +778 -0
  321. package/server/service/validation/board-model-schema.ts +446 -0
  322. package/server/service/validation/tool-validation.ts +228 -0
  323. package/things-factory.config.js +1 -0
  324. package/translations/en.json +49 -0
  325. package/translations/ja.json +48 -0
  326. package/translations/ko.json +49 -0
  327. package/translations/ms.json +48 -0
  328. package/translations/zh.json +48 -0
  329. package/tsconfig.json +9 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * clear-tools — clearStyle 의 aspect 별 reset 동작.
3
+ *
4
+ * preset 적용 전 / 데이터 바인딩 적용 전 깨끗한 상태로 돌리기 위한 helper.
5
+ *
6
+ * stroke / font 형식은 things-scene canonical 로 정정됨 (이전 stale 잔재 제거).
7
+ * shadow 는 `{ blur, color }` — drawer 가 blur 우선 + blurSize fallback 이라
8
+ * 둘 다 동작하지만 canonical 인 blur 사용.
9
+ */
10
+ import { buildClearStyleOps } from './clear-tools'
11
+
12
+ describe('buildClearStyleOps — 입력 검증', () => {
13
+ test('refids 비어있으면 에러', () => {
14
+ const r = buildClearStyleOps({ refids: [], aspects: ['fill'] })
15
+ expect(r.ops).toEqual([])
16
+ expect(r.errors[0]).toMatch(/non-empty array/)
17
+ })
18
+
19
+ test('비-숫자 refids 필터링', () => {
20
+ const r = buildClearStyleOps({
21
+ refids: [1, NaN, 'x' as any, 2],
22
+ aspects: ['fill']
23
+ })
24
+ expect(r.ops).toHaveLength(2)
25
+ })
26
+
27
+ test('aspects 비어있으면 에러', () => {
28
+ const r = buildClearStyleOps({ refids: [1], aspects: [] })
29
+ expect(r.ops).toEqual([])
30
+ expect(r.errors[0]).toMatch(/aspects must be a non-empty array/)
31
+ })
32
+
33
+ test('알 수 없는 aspect → 에러 + 유효 목록 제시', () => {
34
+ const r = buildClearStyleOps({
35
+ refids: [1],
36
+ aspects: ['glow' as any, 'fill']
37
+ })
38
+ expect(r.ops).toEqual([])
39
+ expect(r.errors[0]).toMatch(/Unknown aspect\(s\): glow/)
40
+ expect(r.errors[0]).toMatch(/fill/)
41
+ })
42
+ })
43
+
44
+ describe('buildClearStyleOps — aspect 별 reset patch (현재 동작)', () => {
45
+ test('fill → fillStyle: { type: none }', () => {
46
+ const r = buildClearStyleOps({ refids: [1], aspects: ['fill'] })
47
+ expect(r.ops).toHaveLength(1)
48
+ expect(r.ops[0].patch).toEqual({ fillStyle: { type: 'none' } })
49
+ })
50
+
51
+ test('opacity → alpha: 1', () => {
52
+ const r = buildClearStyleOps({ refids: [1], aspects: ['opacity'] })
53
+ expect(r.ops[0].patch).toEqual({ alpha: 1 })
54
+ })
55
+
56
+ test('material → material3d: { preset: default }', () => {
57
+ const r = buildClearStyleOps({ refids: [1], aspects: ['material'] })
58
+ expect(r.ops[0].patch).toEqual({ material3d: { preset: 'default' } })
59
+ })
60
+
61
+ test('stroke → lineWidth: 0 (top-level)', () => {
62
+ const r = buildClearStyleOps({ refids: [1], aspects: ['stroke'] })
63
+ expect(r.ops[0].patch).toEqual({ lineWidth: 0 })
64
+ })
65
+
66
+ test('shadow → blur: 0, color: transparent', () => {
67
+ const r = buildClearStyleOps({ refids: [1], aspects: ['shadow'] })
68
+ expect(r.ops[0].patch).toEqual({ shadow: { blur: 0, color: 'transparent' } })
69
+ })
70
+
71
+ test('font → 핵심 폰트 키 null=delete', () => {
72
+ const r = buildClearStyleOps({ refids: [1], aspects: ['font'] })
73
+ expect(r.ops[0].patch).toEqual({
74
+ fontSize: null,
75
+ fontFamily: null,
76
+ fontColor: null,
77
+ fontWeight: null,
78
+ fontStyle: null
79
+ })
80
+ })
81
+
82
+ test('font + 다른 aspect 조합 — 양쪽 모두 효과 있음', () => {
83
+ const r = buildClearStyleOps({
84
+ refids: [1],
85
+ aspects: ['font', 'opacity']
86
+ })
87
+ expect(r.ops).toHaveLength(1)
88
+ expect(r.ops[0].patch).toEqual({
89
+ fontSize: null,
90
+ fontFamily: null,
91
+ fontColor: null,
92
+ fontWeight: null,
93
+ fontStyle: null,
94
+ alpha: 1
95
+ })
96
+ })
97
+ })
98
+
99
+ describe('buildClearStyleOps — 다중 aspect 병합', () => {
100
+ test('fill + opacity 한 번에', () => {
101
+ const r = buildClearStyleOps({
102
+ refids: [1],
103
+ aspects: ['fill', 'opacity']
104
+ })
105
+ expect(r.ops).toHaveLength(1)
106
+ // 두 aspect 의 patch 가 한 객체로 병합
107
+ expect(r.ops[0].patch).toEqual({
108
+ fillStyle: { type: 'none' },
109
+ alpha: 1
110
+ })
111
+ })
112
+
113
+ test('fill + material', () => {
114
+ const r = buildClearStyleOps({
115
+ refids: [1],
116
+ aspects: ['fill', 'material']
117
+ })
118
+ expect(r.ops[0].patch).toEqual({
119
+ fillStyle: { type: 'none' },
120
+ material3d: { preset: 'default' }
121
+ })
122
+ })
123
+
124
+ test('모든 aspect 한 번에 — 6개 모두 병합', () => {
125
+ const r = buildClearStyleOps({
126
+ refids: [1],
127
+ aspects: ['fill', 'stroke', 'shadow', 'material', 'font', 'opacity']
128
+ })
129
+ expect(r.ops).toHaveLength(1)
130
+ const p = r.ops[0].patch as any
131
+ expect(p.fillStyle).toEqual({ type: 'none' })
132
+ expect(p.lineWidth).toBe(0)
133
+ expect(p.shadow).toEqual({ blur: 0, color: 'transparent' })
134
+ expect(p.material3d).toEqual({ preset: 'default' })
135
+ expect(p.fontSize).toBeNull()
136
+ expect(p.alpha).toBe(1)
137
+ })
138
+ })
139
+
140
+ describe('buildClearStyleOps — multi-target', () => {
141
+ test('동일 reset patch 가 여러 refid 에 분배', () => {
142
+ const r = buildClearStyleOps({
143
+ refids: [10, 20, 30],
144
+ aspects: ['fill']
145
+ })
146
+ expect(r.ops).toHaveLength(3)
147
+ expect(r.ops.every(o => (o as any).patch.fillStyle.type === 'none')).toBe(true)
148
+ })
149
+ })
150
+
151
+ describe('buildClearStyleOps — 시나리오', () => {
152
+ test('preset 적용 전 cleanse — fill + stroke + material', () => {
153
+ const r = buildClearStyleOps({
154
+ refids: [1, 2, 3],
155
+ aspects: ['fill', 'material']
156
+ })
157
+ expect(r.ops).toHaveLength(3)
158
+ expect(r.ops[0].patch).toMatchObject({
159
+ fillStyle: { type: 'none' },
160
+ material3d: { preset: 'default' }
161
+ })
162
+ })
163
+ })
164
+
@@ -0,0 +1,69 @@
1
+ /**
2
+ * clearStyle — 특정 styling aspect 들을 초기화 (제거).
3
+ *
4
+ * preset 적용 전 / 데이터 바인딩 적용 전 깨끗한 상태로 돌리기 위한 helper.
5
+ */
6
+ import type { BoardEditOp } from '../types'
7
+
8
+ export interface ClearStyleInput {
9
+ refids: number[]
10
+ aspects: Array<'fill' | 'stroke' | 'shadow' | 'material' | 'font' | 'opacity'>
11
+ }
12
+
13
+ export interface OpsResult {
14
+ ops: BoardEditOp[]
15
+ errors: string[]
16
+ }
17
+
18
+ const ASPECT_TO_PATCH: Record<string, any> = {
19
+ fill: { fillStyle: { type: 'none' } },
20
+ // stroke: strokeStyle 는 색상 문자열 (top-level), 굵기는 lineWidth (top-level).
21
+ // 굵기 0 으로 윤곽선 시각 제거 (색은 유지). 완전 제거는 strokeStyle: null 동시 set.
22
+ stroke: { lineWidth: 0 },
23
+ shadow: { shadow: { blur: 0, color: 'transparent' } },
24
+ material: { material3d: { preset: 'default' } },
25
+ // font: null = delete 시맨틱 (apply-patch.ts:233-243). 핵심 폰트 키 모두 제거 →
26
+ // component 가 기본 폰트 동작으로 복귀. 보존이 의도라면 font aspect 미선택.
27
+ font: {
28
+ fontSize: null,
29
+ fontFamily: null,
30
+ fontColor: null,
31
+ fontWeight: null,
32
+ fontStyle: null
33
+ },
34
+ opacity: { alpha: 1 }
35
+ }
36
+
37
+ export function buildClearStyleOps(input: ClearStyleInput): OpsResult {
38
+ if (!Array.isArray(input.refids) || input.refids.length === 0) {
39
+ return { ops: [], errors: ['refids must be a non-empty array'] }
40
+ }
41
+ const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))
42
+ if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }
43
+
44
+ if (!Array.isArray(input.aspects) || input.aspects.length === 0) {
45
+ return { ops: [], errors: ['aspects must be a non-empty array'] }
46
+ }
47
+ const valid = ['fill', 'stroke', 'shadow', 'material', 'font', 'opacity']
48
+ const unknown = input.aspects.filter(a => !valid.includes(a))
49
+ if (unknown.length > 0) {
50
+ return {
51
+ ops: [],
52
+ errors: [`Unknown aspect(s): ${unknown.join(', ')}. Valid: ${valid.join('/')}`]
53
+ }
54
+ }
55
+
56
+ // 모든 aspect 의 patch 를 하나로 병합 (한 refid 당 하나의 modify op)
57
+ const merged: any = {}
58
+ for (const a of input.aspects) {
59
+ Object.assign(merged, ASPECT_TO_PATCH[a])
60
+ }
61
+ if (Object.keys(merged).length === 0) {
62
+ return { ops: [], errors: ['No effective patch — try other aspects'] }
63
+ }
64
+
65
+ return {
66
+ ops: refids.map(refid => ({ op: 'modify', refid, patch: merged })),
67
+ errors: []
68
+ }
69
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * color-utils — 다양한 색상 입력 → 정규 hex / alpha 분리.
3
+ *
4
+ * AI 가 다양한 자연어 / 형식으로 색상 표현 시 모두 안전 처리.
5
+ */
6
+ import { parseColor, normalizeColor, validateColor } from './color-utils'
7
+
8
+ describe('parseColor — hex', () => {
9
+ test('hex 6자리 정규화', () => {
10
+ expect(parseColor('#ff0000')).toEqual({ hex: '#ff0000', alpha: 1 })
11
+ expect(parseColor('#FF0000')).toEqual({ hex: '#ff0000', alpha: 1 })
12
+ })
13
+
14
+ test('hex 3자리 → 6자리 확장', () => {
15
+ expect(parseColor('#f00')).toEqual({ hex: '#ff0000', alpha: 1 })
16
+ expect(parseColor('#0Af')).toEqual({ hex: '#00aaff', alpha: 1 })
17
+ })
18
+
19
+ test('hex 8자리 → alpha 분리', () => {
20
+ expect(parseColor('#ff000080')).toEqual({ hex: '#ff0000', alpha: 0.502 })
21
+ })
22
+
23
+ test('hex 4자리 → 8자리 확장', () => {
24
+ expect(parseColor('#f00f')).toEqual({ hex: '#ff0000', alpha: 1 })
25
+ })
26
+
27
+ test('잘못된 hex → null', () => {
28
+ expect(parseColor('#zzz')).toBeNull()
29
+ expect(parseColor('#12345')).toBeNull()
30
+ expect(parseColor('##ff0000')).toBeNull()
31
+ })
32
+ })
33
+
34
+ describe('parseColor — rgb / rgba', () => {
35
+ test('rgb 정수', () => {
36
+ expect(parseColor('rgb(255, 0, 0)')).toEqual({ hex: '#ff0000', alpha: 1 })
37
+ expect(parseColor('rgb(0,128,255)')).toEqual({ hex: '#0080ff', alpha: 1 })
38
+ })
39
+
40
+ test('rgba 알파 포함', () => {
41
+ expect(parseColor('rgba(255, 0, 0, 0.5)')).toEqual({ hex: '#ff0000', alpha: 0.5 })
42
+ expect(parseColor('rgba(0,0,0,0)')).toEqual({ hex: '#000000', alpha: 0 })
43
+ })
44
+
45
+ test('범위 밖 — null', () => {
46
+ expect(parseColor('rgb(300, 0, 0)')).toBeNull()
47
+ expect(parseColor('rgba(0, 0, 0, 1.5)')).toBeNull()
48
+ })
49
+ })
50
+
51
+ describe('parseColor — named / semantic', () => {
52
+ test('CSS named — 기본', () => {
53
+ expect(parseColor('red')).toEqual({ hex: '#ff0000', alpha: 1 })
54
+ expect(parseColor('white')).toEqual({ hex: '#ffffff', alpha: 1 })
55
+ expect(parseColor('navy')).toEqual({ hex: '#000080', alpha: 1 })
56
+ })
57
+
58
+ test('CSS named — 대소문자 무관', () => {
59
+ expect(parseColor('RED')).toEqual({ hex: '#ff0000', alpha: 1 })
60
+ expect(parseColor('Red')).toEqual({ hex: '#ff0000', alpha: 1 })
61
+ })
62
+
63
+ test('semantic alias', () => {
64
+ expect(parseColor('warm-red')).toEqual({ hex: '#e74c3c', alpha: 1 })
65
+ expect(parseColor('forest-green')).toEqual({ hex: '#228b22', alpha: 1 })
66
+ expect(parseColor('success')).toEqual({ hex: '#22c55e', alpha: 1 })
67
+ })
68
+
69
+ test('알 수 없는 이름 → null', () => {
70
+ expect(parseColor('reddish')).toBeNull()
71
+ expect(parseColor('not-a-color')).toBeNull()
72
+ })
73
+ })
74
+
75
+ describe('parseColor — transparent / 특수', () => {
76
+ test('transparent', () => {
77
+ expect(parseColor('transparent')).toEqual({ hex: 'transparent', alpha: 0 })
78
+ expect(parseColor('none')).toEqual({ hex: 'transparent', alpha: 0 })
79
+ })
80
+
81
+ test('빈 문자열 / null / undefined', () => {
82
+ expect(parseColor('')).toBeNull()
83
+ expect(parseColor(null)).toBeNull()
84
+ expect(parseColor(undefined)).toBeNull()
85
+ })
86
+
87
+ test('공백 trim', () => {
88
+ expect(parseColor(' red ')).toEqual({ hex: '#ff0000', alpha: 1 })
89
+ expect(parseColor(' #ff0000 ')).toEqual({ hex: '#ff0000', alpha: 1 })
90
+ })
91
+ })
92
+
93
+ describe('normalizeColor — 저장용 hex', () => {
94
+ test('정상 → 표준 hex', () => {
95
+ expect(normalizeColor('red')).toBe('#ff0000')
96
+ expect(normalizeColor('#F00')).toBe('#ff0000')
97
+ expect(normalizeColor('rgb(255,0,0)')).toBe('#ff0000')
98
+ expect(normalizeColor('warm-red')).toBe('#e74c3c')
99
+ })
100
+
101
+ test('실패 → null', () => {
102
+ expect(normalizeColor('invalid')).toBeNull()
103
+ expect(normalizeColor('')).toBeNull()
104
+ })
105
+ })
106
+
107
+ describe('validateColor — 정정 제안', () => {
108
+ test('정상 색상', () => {
109
+ expect(validateColor('red').valid).toBe(true)
110
+ expect(validateColor('#ff0000').valid).toBe(true)
111
+ })
112
+
113
+ test('잘못된 색상 — error 메시지 + suggestion', () => {
114
+ const r = validateColor('reddish')
115
+ expect(r.valid).toBe(false)
116
+ expect(r.error).toContain('Invalid color')
117
+ expect(r.suggestion).toBe('red')
118
+ })
119
+
120
+ test('완전 알 수 없는 색 — error 만', () => {
121
+ const r = validateColor('xyzabc')
122
+ expect(r.valid).toBe(false)
123
+ expect(r.error).toBeDefined()
124
+ })
125
+
126
+ test('null/undefined 안전', () => {
127
+ expect(validateColor(null).valid).toBe(false)
128
+ expect(validateColor(undefined).valid).toBe(false)
129
+ })
130
+ })
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Color 파싱 / 정규화 유틸 — styling tool 들이 공통으로 사용.
3
+ *
4
+ * AI 가 자연어로 색상 표현하는 다양한 형태를 받아 표준 hex 또는 rgba 로 변환.
5
+ *
6
+ * 지원 입력:
7
+ * - hex 6자리: '#ff0000', '#FF0000'
8
+ * - hex 3자리: '#f00' (→ '#ff0000')
9
+ * - hex 8자리: '#ff0000ff' (alpha 포함)
10
+ * - rgb / rgba: 'rgb(255,0,0)', 'rgba(255,0,0,0.5)'
11
+ * - CSS named: 'red', 'transparent', ...
12
+ * - 시맨틱 alias: 'warm-red', 'forest-green', ...
13
+ *
14
+ * 출력 형태:
15
+ * - normalizeColor(): 표준 hex (#rrggbb), opacity 는 별개로 분리 반환
16
+ * - validateColor(): 유효성만 체크 (boolean + 에러 메시지)
17
+ */
18
+
19
+ /** CSS named colors → hex (자주 쓰는 것만, 필요 시 확장) */
20
+ const NAMED_COLORS: Record<string, string> = {
21
+ // 무채
22
+ white: '#ffffff',
23
+ black: '#000000',
24
+ gray: '#808080',
25
+ grey: '#808080',
26
+ silver: '#c0c0c0',
27
+ // 기본 컬러
28
+ red: '#ff0000',
29
+ green: '#008000',
30
+ blue: '#0000ff',
31
+ yellow: '#ffff00',
32
+ cyan: '#00ffff',
33
+ magenta: '#ff00ff',
34
+ orange: '#ffa500',
35
+ purple: '#800080',
36
+ pink: '#ffc0cb',
37
+ brown: '#a52a2a',
38
+ // 자주 쓰는 변형
39
+ navy: '#000080',
40
+ teal: '#008080',
41
+ lime: '#00ff00',
42
+ olive: '#808000',
43
+ maroon: '#800000',
44
+ // 투명
45
+ transparent: 'transparent'
46
+ }
47
+
48
+ /**
49
+ * 시맨틱 컬러 alias — AI 가 자연어 표현 사용 가능.
50
+ * Material 3 기반 톤 (brightness 적당, saturation 풍부).
51
+ */
52
+ const SEMANTIC_COLORS: Record<string, string> = {
53
+ // warm reds
54
+ 'warm-red': '#e74c3c',
55
+ 'soft-red': '#ef4444',
56
+ 'deep-red': '#b91c1c',
57
+ 'dark-red': '#7f1d1d',
58
+ // greens
59
+ 'forest-green': '#228b22',
60
+ 'mint-green': '#10b981',
61
+ 'olive-green': '#65a30d',
62
+ 'lime-green': '#84cc16',
63
+ // blues
64
+ 'sky-blue': '#0ea5e9',
65
+ 'navy-blue': '#1e40af',
66
+ 'royal-blue': '#3b82f6',
67
+ 'powder-blue': '#bfdbfe',
68
+ // yellows / oranges
69
+ 'amber': '#f59e0b',
70
+ 'sunset-orange': '#f97316',
71
+ 'gold': '#eab308',
72
+ 'mustard': '#ca8a04',
73
+ // purples
74
+ 'violet': '#7c3aed',
75
+ 'lavender': '#c4b5fd',
76
+ // grays
77
+ 'slate': '#64748b',
78
+ 'charcoal': '#374151',
79
+ 'cool-gray': '#6b7280',
80
+ 'warm-gray': '#78716c',
81
+ // 시맨틱 의도
82
+ success: '#22c55e',
83
+ warning: '#f59e0b',
84
+ error: '#ef4444',
85
+ info: '#3b82f6'
86
+ }
87
+
88
+ /** Parse 결과 — color 와 alpha 분리. alpha 는 0..1. */
89
+ export interface ParsedColor {
90
+ /** 표준 hex (#rrggbb, 소문자) 또는 'transparent' */
91
+ hex: string
92
+ /** Alpha 채널 0..1. transparent 면 0, 명시 alpha 없으면 1. */
93
+ alpha: number
94
+ }
95
+
96
+ /**
97
+ * 색상 입력을 정규 hex + alpha 로 변환.
98
+ * 잘못된 입력은 null — 호출자가 에러 처리.
99
+ */
100
+ export function parseColor(input: string | null | undefined): ParsedColor | null {
101
+ if (typeof input !== 'string') return null
102
+ const s = input.trim().toLowerCase()
103
+ if (s.length === 0) return null
104
+
105
+ // transparent
106
+ if (s === 'transparent' || s === 'none') {
107
+ return { hex: 'transparent', alpha: 0 }
108
+ }
109
+
110
+ // hex
111
+ if (s.startsWith('#')) {
112
+ return parseHex(s)
113
+ }
114
+
115
+ // rgb / rgba
116
+ const rgbMatch = s.match(/^rgba?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/)
117
+ if (rgbMatch) {
118
+ const r = clampByte(rgbMatch[1])
119
+ const g = clampByte(rgbMatch[2])
120
+ const b = clampByte(rgbMatch[3])
121
+ const a = rgbMatch[4] !== undefined ? clampUnit(rgbMatch[4]) : 1
122
+ if (r === null || g === null || b === null || a === null) return null
123
+ return { hex: rgbToHex(r, g, b), alpha: a }
124
+ }
125
+
126
+ // CSS named
127
+ if (NAMED_COLORS[s]) {
128
+ return parseHex(NAMED_COLORS[s])
129
+ }
130
+
131
+ // 시맨틱 alias
132
+ if (SEMANTIC_COLORS[s]) {
133
+ return parseHex(SEMANTIC_COLORS[s])
134
+ }
135
+
136
+ return null
137
+ }
138
+
139
+ function parseHex(s: string): ParsedColor | null {
140
+ const hex = s.startsWith('#') ? s.slice(1) : s
141
+ if (hex === 'transparent') return { hex: 'transparent', alpha: 0 }
142
+
143
+ if (!/^[0-9a-f]+$/i.test(hex)) return null
144
+
145
+ if (hex.length === 3) {
146
+ // #abc → #aabbcc
147
+ const r = hex[0] + hex[0]
148
+ const g = hex[1] + hex[1]
149
+ const b = hex[2] + hex[2]
150
+ return { hex: `#${r}${g}${b}`.toLowerCase(), alpha: 1 }
151
+ }
152
+ if (hex.length === 4) {
153
+ // #abcd → #aabbccdd
154
+ const r = hex[0] + hex[0]
155
+ const g = hex[1] + hex[1]
156
+ const b = hex[2] + hex[2]
157
+ const a = parseInt(hex[3] + hex[3], 16) / 255
158
+ return { hex: `#${r}${g}${b}`.toLowerCase(), alpha: round3(a) }
159
+ }
160
+ if (hex.length === 6) {
161
+ return { hex: `#${hex}`.toLowerCase(), alpha: 1 }
162
+ }
163
+ if (hex.length === 8) {
164
+ const rgb = hex.slice(0, 6)
165
+ const a = parseInt(hex.slice(6, 8), 16) / 255
166
+ return { hex: `#${rgb}`.toLowerCase(), alpha: round3(a) }
167
+ }
168
+ return null
169
+ }
170
+
171
+ function clampByte(s: string): number | null {
172
+ const n = Number(s)
173
+ if (!Number.isFinite(n) || n < 0 || n > 255) return null
174
+ return Math.round(n)
175
+ }
176
+
177
+ function clampUnit(s: string): number | null {
178
+ const n = Number(s)
179
+ if (!Number.isFinite(n) || n < 0 || n > 1) return null
180
+ return n
181
+ }
182
+
183
+ function rgbToHex(r: number, g: number, b: number): string {
184
+ const h = (n: number) => n.toString(16).padStart(2, '0')
185
+ return `#${h(r)}${h(g)}${h(b)}`
186
+ }
187
+
188
+ function round3(n: number): number {
189
+ return Math.round(n * 1000) / 1000
190
+ }
191
+
192
+ /**
193
+ * 단순 검증 — 정규화 가능 여부만.
194
+ * 에러 메시지에 정정 제안 포함 가능.
195
+ */
196
+ export function validateColor(input: string | null | undefined): {
197
+ valid: boolean
198
+ error?: string
199
+ suggestion?: string
200
+ } {
201
+ if (typeof input !== 'string') {
202
+ return { valid: false, error: 'Color must be a string.' }
203
+ }
204
+ const parsed = parseColor(input)
205
+ if (parsed) return { valid: true }
206
+
207
+ // 비슷한 named color 추천
208
+ const lower = input.trim().toLowerCase()
209
+ const allKnown = [...Object.keys(NAMED_COLORS), ...Object.keys(SEMANTIC_COLORS)]
210
+ const close = allKnown.find(k => k.includes(lower) || lower.includes(k))
211
+ return {
212
+ valid: false,
213
+ error: `Invalid color: '${input}'. Use hex (#ff0000), rgb(255,0,0), or named (red, warm-red, ...).`,
214
+ suggestion: close
215
+ }
216
+ }
217
+
218
+ /**
219
+ * 색상을 hex 문자열로 정규화 (저장용).
220
+ * 실패 시 null.
221
+ */
222
+ export function normalizeColor(input: string | null | undefined): string | null {
223
+ const parsed = parseColor(input)
224
+ if (!parsed) return null
225
+ return parsed.hex
226
+ }