@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.
- package/PLAN-styling-tools.md +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- 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
|
+
}
|