@things-factory/figure-ui 10.1.3

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 (112) hide show
  1. package/client/bootstrap.ts +16 -0
  2. package/client/graphql/index.ts +209 -0
  3. package/client/index.ts +2 -0
  4. package/client/modeller/figure-ask.ts +247 -0
  5. package/client/modeller/figure-canvas.ts +482 -0
  6. package/client/modeller/figure-inspector.ts +1054 -0
  7. package/client/modeller/figure-parts.ts +479 -0
  8. package/client/modeller/figure-preview.ts +381 -0
  9. package/client/modeller/figure-report.ts +895 -0
  10. package/client/modeller/figure-settings.ts +285 -0
  11. package/client/modeller/figure-side.ts +162 -0
  12. package/client/modeller/figure-source.test.ts +336 -0
  13. package/client/modeller/figure-source.ts +338 -0
  14. package/client/modeller/figure-view.ts +79 -0
  15. package/client/modeller/maturity.ts +288 -0
  16. package/client/modeller/part-edits.test.ts +252 -0
  17. package/client/modeller/part-edits.ts +246 -0
  18. package/client/modeller/proposal.ts +142 -0
  19. package/client/modeller/scene-budget.ts +102 -0
  20. package/client/pages/figure-list-page.ts +274 -0
  21. package/client/pages/figure-modeller-page.ts +1067 -0
  22. package/client/route.ts +11 -0
  23. package/client/themes/brand.css +51 -0
  24. package/client/themes/dark.css +64 -0
  25. package/client/themes/light.css +95 -0
  26. package/client/tsconfig.json +19 -0
  27. package/client/types.ts +96 -0
  28. package/client/viewparts/figure-card.ts +226 -0
  29. package/dist-client/bootstrap.d.ts +14 -0
  30. package/dist-client/bootstrap.js +17 -0
  31. package/dist-client/bootstrap.js.map +1 -0
  32. package/dist-client/graphql/index.d.ts +41 -0
  33. package/dist-client/graphql/index.js +173 -0
  34. package/dist-client/graphql/index.js.map +1 -0
  35. package/dist-client/index.d.ts +2 -0
  36. package/dist-client/index.js +3 -0
  37. package/dist-client/index.js.map +1 -0
  38. package/dist-client/modeller/figure-ask.d.ts +36 -0
  39. package/dist-client/modeller/figure-ask.js +256 -0
  40. package/dist-client/modeller/figure-ask.js.map +1 -0
  41. package/dist-client/modeller/figure-canvas.d.ts +128 -0
  42. package/dist-client/modeller/figure-canvas.js +431 -0
  43. package/dist-client/modeller/figure-canvas.js.map +1 -0
  44. package/dist-client/modeller/figure-inspector.d.ts +149 -0
  45. package/dist-client/modeller/figure-inspector.js +991 -0
  46. package/dist-client/modeller/figure-inspector.js.map +1 -0
  47. package/dist-client/modeller/figure-parts.d.ts +64 -0
  48. package/dist-client/modeller/figure-parts.js +478 -0
  49. package/dist-client/modeller/figure-parts.js.map +1 -0
  50. package/dist-client/modeller/figure-preview.d.ts +62 -0
  51. package/dist-client/modeller/figure-preview.js +360 -0
  52. package/dist-client/modeller/figure-preview.js.map +1 -0
  53. package/dist-client/modeller/figure-report.d.ts +134 -0
  54. package/dist-client/modeller/figure-report.js +881 -0
  55. package/dist-client/modeller/figure-report.js.map +1 -0
  56. package/dist-client/modeller/figure-settings.d.ts +39 -0
  57. package/dist-client/modeller/figure-settings.js +277 -0
  58. package/dist-client/modeller/figure-settings.js.map +1 -0
  59. package/dist-client/modeller/figure-side.d.ts +41 -0
  60. package/dist-client/modeller/figure-side.js +175 -0
  61. package/dist-client/modeller/figure-side.js.map +1 -0
  62. package/dist-client/modeller/figure-source.d.ts +196 -0
  63. package/dist-client/modeller/figure-source.js +153 -0
  64. package/dist-client/modeller/figure-source.js.map +1 -0
  65. package/dist-client/modeller/figure-view.d.ts +57 -0
  66. package/dist-client/modeller/figure-view.js +36 -0
  67. package/dist-client/modeller/figure-view.js.map +1 -0
  68. package/dist-client/modeller/maturity.d.ts +55 -0
  69. package/dist-client/modeller/maturity.js +212 -0
  70. package/dist-client/modeller/maturity.js.map +1 -0
  71. package/dist-client/modeller/part-edits.d.ts +64 -0
  72. package/dist-client/modeller/part-edits.js +218 -0
  73. package/dist-client/modeller/part-edits.js.map +1 -0
  74. package/dist-client/modeller/proposal.d.ts +45 -0
  75. package/dist-client/modeller/proposal.js +96 -0
  76. package/dist-client/modeller/proposal.js.map +1 -0
  77. package/dist-client/modeller/scene-budget.d.ts +78 -0
  78. package/dist-client/modeller/scene-budget.js +80 -0
  79. package/dist-client/modeller/scene-budget.js.map +1 -0
  80. package/dist-client/pages/figure-list-page.d.ts +40 -0
  81. package/dist-client/pages/figure-list-page.js +280 -0
  82. package/dist-client/pages/figure-list-page.js.map +1 -0
  83. package/dist-client/pages/figure-modeller-page.d.ts +219 -0
  84. package/dist-client/pages/figure-modeller-page.js +1045 -0
  85. package/dist-client/pages/figure-modeller-page.js.map +1 -0
  86. package/dist-client/route.d.ts +1 -0
  87. package/dist-client/route.js +11 -0
  88. package/dist-client/route.js.map +1 -0
  89. package/dist-client/themes/brand.css +51 -0
  90. package/dist-client/themes/dark.css +64 -0
  91. package/dist-client/themes/light.css +95 -0
  92. package/dist-client/types.d.ts +93 -0
  93. package/dist-client/types.js +2 -0
  94. package/dist-client/types.js.map +1 -0
  95. package/dist-client/viewparts/figure-card.d.ts +24 -0
  96. package/dist-client/viewparts/figure-card.js +221 -0
  97. package/dist-client/viewparts/figure-card.js.map +1 -0
  98. package/dist-server/index.d.ts +7 -0
  99. package/dist-server/index.js +3 -0
  100. package/dist-server/index.js.map +1 -0
  101. package/package.json +41 -0
  102. package/server/index.ts +7 -0
  103. package/server/tsconfig.json +18 -0
  104. package/test/brand-palette.test.ts +188 -0
  105. package/test/heading-scale-guard.test.ts +144 -0
  106. package/test/resolve-ts-siblings.mjs +67 -0
  107. package/things-factory.config.js +11 -0
  108. package/translations/en.json +247 -0
  109. package/translations/ja.json +247 -0
  110. package/translations/ko.json +247 -0
  111. package/translations/ms.json +247 -0
  112. package/translations/zh.json +247 -0
@@ -0,0 +1,11 @@
1
+ export default function route(page: string) {
2
+ switch (page) {
3
+ case 'figure-list':
4
+ import('./pages/figure-list-page')
5
+ return page
6
+
7
+ case 'figure-modeller':
8
+ import('./pages/figure-modeller-page')
9
+ return page
10
+ }
11
+ }
@@ -0,0 +1,51 @@
1
+ /*
2
+ * figure 의 **브랜드 색 — 정본은 이 파일 하나다.**
3
+ *
4
+ * ── 왜 이 파일이 생겼나 ────────────────────────────────────────────────────
5
+ * figure-ui 에는 팔레트가 없었다. 호스트 앱이 주는 `--md-sys-color-*` 를 그대로 받아 썼고,
6
+ * 하네스(`test/browser/modeller-page.mjs`)는 자기 `:root` 에 값을 직접 적고 있었다. 그래서
7
+ * 하네스에서 본 색과 앱에서 볼 색이 **아무 오류 없이 달라질 수 있는** 상태였다.
8
+ *
9
+ * operato-plant 가 같은 문제를 겪고 같은 방식으로 풀었다(`operato-plant/assets/brand.css`).
10
+ * 그 구조를 그대로 따른다 — 값은 여기 한 번만 적고, 앱 팔레트와 하네스가 이 파일을 읽는다.
11
+ *
12
+ * ── 브랜드 색만 여기 둔다 ──────────────────────────────────────────────────
13
+ * 중성색은 여기 없다. 그것은 `client/themes/light.css` 가 갖는다. 여기 있는 것은 **제품을
14
+ * 가리키는 색**뿐이고, 그것만 절대 갈라지면 안 된다.
15
+ *
16
+ * ── 밝은 것과 어두운 것을 함께 든다 ────────────────────────────────────────
17
+ * 어두운 바탕에서 `#154473` 은 가라앉고(대비 1.6), 밝은 바탕에서 `#64a9f3` 은 글자에 못 쓴다
18
+ * (대비 2.4). 그래서 같은 이름이 밝기마다 다른 값을 갖는다 — 이름이 뜻이고 값은 자리에 따라
19
+ * 다르다. plant · twin 과 같은 규약이다.
20
+ *
21
+ * ── 색은 하나뿐이다 ────────────────────────────────────────────────────────
22
+ * 한동안 primary(네이비)와 accent(따로 고른 색)를 둘 두려 했다. 그러면 그 둘이 서로 안
23
+ * 부딪히게 하느라 화면이 계속 시끄러워진다. twin 도 plant 도 브랜드 색을 하나만 두고,
24
+ * 그것이 곧 primary 다. 여기도 그렇게 한다.
25
+ *
26
+ * 나머지는 뜻이 있는 색이다 — 주의(warn) · 오류(error). 그 둘은 브랜드가 아니라 상태이므로
27
+ * 팔레트 파일에 있다.
28
+ */
29
+
30
+ :root {
31
+ /*
32
+ * figure — 네이비 252°.
33
+ *
34
+ * OKLCH L* 38 · C 0.095 · H 252. 흰 글자 대비 9.97 로 작은 글자 기준(4.5)에 여유가 크다.
35
+ *
36
+ * 채도를 낮게 잡은 것이 이 색의 요점이다. 이 도구의 화면에서 색이 말해야 하는 것은 **저작자가
37
+ * 고른 부품 색**이다. 틀이 진한 색으로 소리를 내면 캔버스 안의 색을 판단할 수 없다. 그래서
38
+ * 브랜드 색은 깊고 조용하게 두고, 고른 것의 밑줄 · 기본 단추 · 링크에만 좁게 쓴다.
39
+ */
40
+ --brand-figure: #154473;
41
+
42
+ /*
43
+ * 어두운 면 위에서 쓰는 같은 색. L* 72 · C 0.130.
44
+ *
45
+ * 3D 캔버스의 선택 테두리와 어두운 배경의 강조가 이 값이다. 어두운 면(#12161b) 대비 7.02.
46
+ */
47
+ --brand-figure-2: #64a9f3;
48
+
49
+ /** 아주 옅게 깔 때 — 고른 줄의 바탕. 위 색과 같은 색상각이다. */
50
+ --brand-figure-wash: #eaf3fd;
51
+ }
@@ -0,0 +1,64 @@
1
+ @import url('./brand.css');
2
+
3
+ /*
4
+ * figure 다크 팔레트.
5
+ *
6
+ * 라이트 쪽 머리말이 규칙의 정본이다(`light.css`). 여기서는 **어두운 면에서 달라지는 것만**
7
+ * 적는다.
8
+ *
9
+ * ── 브랜드 색이 밝은 쪽으로 바뀐다 ────────────────────────────────────────
10
+ * `#154473` 은 어두운 바탕에서 대비 1.6 이라 보이지 않는다. 그래서 어두운 면에서는
11
+ * `--brand-figure-2`(#64a9f3)가 브랜드 색이다. 이름이 뜻이고 값은 자리에 따라 다르다 —
12
+ * plant · twin 과 같은 규약이다.
13
+ *
14
+ * [측정] 어두운 면(#12161b) 대비 — #154473 1.60 · #64a9f3 7.02
15
+ *
16
+ * ── 캔버스는 여기서도 화면에서 가장 어두운 면이 아니다 ────────────────────
17
+ * 라이트에서 캔버스를 한 단 내렸던 이유가 어두운 쪽에서는 뒤집힌다. 부품이 묻히지 않으려면
18
+ * 캔버스가 표면보다 **밝아야** 한다. 어두운 부품(palette.dark)이 바탕과 같아지면 형태가
19
+ * 사라진다.
20
+ */
21
+
22
+ :root[theme='dark'],
23
+ :host([theme='dark']) {
24
+ /* ── 표면 ────────────────────────────────────────────────────────────── */
25
+ --md-sys-color-surface: #12161b;
26
+ --md-sys-color-surface-container-lowest: #0d1116;
27
+ --md-sys-color-surface-container: #1a1f25;
28
+ --md-sys-color-surface-container-highest: #252b32;
29
+
30
+ /** 캔버스 — 표면보다 한 단 밝다. 어두운 부품이 바탕에 묻히지 않게 한다. */
31
+ --md-sys-color-surface-variant: #2f353d;
32
+
33
+ /* ── 선 ──────────────────────────────────────────────────────────────── */
34
+ --md-sys-color-outline-variant: #333941;
35
+ --md-sys-color-outline: #59606a;
36
+
37
+ /* ── 글자 셋 ─────────────────────────────────────────────────────────── */
38
+ --md-sys-color-on-surface: #e3e7ec;
39
+ --md-sys-color-on-surface-variant: #a8afb8;
40
+ --md-sys-color-on-surface-dim: #7d848d;
41
+
42
+ /* ── 브랜드 ──────────────────────────────────────────────────────────── */
43
+ --md-sys-color-primary: var(--brand-figure-2);
44
+ --md-sys-color-on-primary: #0d1116;
45
+ --md-sys-color-inverse-primary: var(--brand-figure);
46
+
47
+ --md-sys-color-primary-container: #1c2b3d;
48
+ --md-sys-color-on-primary-container: var(--brand-figure-2);
49
+
50
+ --md-sys-color-secondary-container: #1a1f25;
51
+ --md-sys-color-on-secondary-container: #a8afb8;
52
+
53
+ /* ── 뜻이 있는 색 ────────────────────────────────────────────────────── */
54
+ /* 어두운 면에서는 주의·오류도 밝은 쪽 값을 쓴다. 낮은 값은 안 보인다. */
55
+ --md-sys-color-tertiary: #dfa72a;
56
+ --md-sys-color-on-tertiary: #0d1116;
57
+ --md-sys-color-tertiary-container: #1a1f25;
58
+ --md-sys-color-on-tertiary-container: #dfa72a;
59
+
60
+ --md-sys-color-error: #f2837c;
61
+ --md-sys-color-on-error: #0d1116;
62
+ --md-sys-color-error-container: #1a1f25;
63
+ --md-sys-color-on-error-container: #f2837c;
64
+ }
@@ -0,0 +1,95 @@
1
+ @import url('./brand.css');
2
+
3
+ /*
4
+ * figure 라이트 팔레트 — 이 파일이 이 도구 색의 **정본**이다.
5
+ *
6
+ * 브랜드 색은 `client/themes/brand.css` 가 갖는다. 여기서 쓸 수 있는 이름은 `--brand-figure` ·
7
+ * `--brand-figure-2` · `--brand-figure-wash` 셋이다. 값을 여기 다시 적지 않는다.
8
+ *
9
+ * ── 이 도구에서 색이 말해야 하는 것 ────────────────────────────────────────
10
+ * **저작자가 고른 부품 색**이다. 화면 한가운데 캔버스가 있고, 거기 놓인 부품의 색을 보고
11
+ * 판단하는 것이 이 도구의 일이다. 틀이 색으로 소리를 내면 그 판단이 흐려진다.
12
+ *
13
+ * 그래서 틀은 무채색이다. 브랜드 네이비는 세 자리에만 쓴다 — **고른 것의 밑줄 · 기본 단추 ·
14
+ * 링크**. 그 밖에는 회색과 글자색이 전부다.
15
+ *
16
+ * ── 배지에 파스텔을 깔지 않는다 ────────────────────────────────────────────
17
+ * 전에는 등급 배지와 칩이 파스텔 면(연파랑 · 연노랑)이었다. 작은 면에 옅은 색을 깔면 화면이
18
+ * 싸구려로 보이고, 캔버스의 부품 색과도 섞인다. 이제 배지는 **무채색 바탕에 머리카락 선**이고
19
+ * 뜻은 **글자색**이 든다. 그래서 `*-container` 토큰들이 전부 무채색으로 간다 — 컴포넌트를
20
+ * 하나씩 고치지 않아도 이 파일 하나로 그 규칙이 선다.
21
+ *
22
+ * ── 회색은 셋이다 ──────────────────────────────────────────────────────────
23
+ * 본문(ink) · 보조(ink-2) · 흐린 것(ink-3). 넷을 넘기면 어느 것을 써야 할지 매번 고민하게 되고,
24
+ * 결국 아무거나 쓰게 된다. 전에는 단계가 고르지 않아 두 회색이 같은 무게로 읽혔다.
25
+ *
26
+ * [측정] 흰 바탕 대비 — ink 18.16 · ink-2 8.41 · ink-3 5.06. 셋 다 작은 글자 기준(4.5)을 넘는다.
27
+ */
28
+
29
+ :root {
30
+ /* ── 표면 ────────────────────────────────────────────────────────────── */
31
+ --md-sys-color-surface: #ffffff;
32
+ --md-sys-color-surface-container-lowest: #ffffff;
33
+ --md-sys-color-surface-container: #f3f5f8;
34
+ --md-sys-color-surface-container-highest: #e6e9ec;
35
+
36
+ /*
37
+ * 캔버스 — 화면에서 가장 어두운 무채색 면이다.
38
+ *
39
+ * 부품이 그 위에 놓인다. 밝은 회색이면 밝은 부품이 바탕에 묻히고, 부품 색이 실제보다 진해
40
+ * 보인다. 한 단 내려서 위에 놓인 것이 무슨 색이든 제 밝기로 보이게 한다.
41
+ */
42
+ --md-sys-color-surface-variant: #d5d8db;
43
+
44
+ /* ── 선 ──────────────────────────────────────────────────────────────── */
45
+ --md-sys-color-outline-variant: #d8dbdf;
46
+ --md-sys-color-outline: #babec3;
47
+
48
+ /* ── 글자 셋 ─────────────────────────────────────────────────────────── */
49
+ --md-sys-color-on-surface: #12161b;
50
+ --md-sys-color-on-surface-variant: #484e55;
51
+ /** 흐린 것 — 도움말, 단위, 빈 자리 안내. 이것도 4.5 를 넘는다. */
52
+ --md-sys-color-on-surface-dim: #6a6f76;
53
+
54
+ /* ── 브랜드 ──────────────────────────────────────────────────────────── */
55
+ --md-sys-color-primary: var(--brand-figure);
56
+ --md-sys-color-on-primary: #ffffff;
57
+ --md-sys-color-inverse-primary: var(--brand-figure-2);
58
+
59
+ /*
60
+ * 고른 줄의 바탕. 이것만 유일하게 색이 든 면이고, 그것도 거의 흰색이다(L* 96).
61
+ * 그 위에 오는 글자는 브랜드 색이며 대비 8.90 이다.
62
+ */
63
+ --md-sys-color-primary-container: var(--brand-figure-wash);
64
+ --md-sys-color-on-primary-container: var(--brand-figure);
65
+
66
+ /*
67
+ * 두 번째 면 — 파스텔을 쓰지 않으므로 무채색이다. 칩과 배지가 이것을 깐다.
68
+ */
69
+ --md-sys-color-secondary-container: #f3f5f8;
70
+ --md-sys-color-on-secondary-container: #484e55;
71
+
72
+ /* ── 뜻이 있는 색 ────────────────────────────────────────────────────── */
73
+ /*
74
+ * 주의 — 권장 범위를 넘었다. 막지는 않는다.
75
+ *
76
+ * 브랜드가 아니라 상태다. `tertiary` 슬롯에 든 것은 figure-ui 가 그 이름으로 주의 문구를
77
+ * 쓰고 있기 때문이고, 뜻은 주의다.
78
+ *
79
+ * [측정] 흰 바탕 5.53 · 배지 바탕(#f3f5f8) 5.06
80
+ */
81
+ --md-sys-color-tertiary: #866300;
82
+ --md-sys-color-on-tertiary: #ffffff;
83
+ --md-sys-color-tertiary-container: #f3f5f8;
84
+ --md-sys-color-on-tertiary-container: #866300;
85
+
86
+ /*
87
+ * 오류 — 스키마를 어겼다. 저장이 막힌다.
88
+ *
89
+ * [측정] 흰 바탕 7.13 · 배지 바탕 6.53
90
+ */
91
+ --md-sys-color-error: #a9231e;
92
+ --md-sys-color-on-error: #ffffff;
93
+ --md-sys-color-error-container: #f3f5f8;
94
+ --md-sys-color-on-error-container: #a9231e;
95
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "experimentalDecorators": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "declaration": true,
8
+ "baseUrl": "./",
9
+ "module": "esnext",
10
+ "outDir": "../dist-client"
11
+ },
12
+ "include": [
13
+ "./**/*"
14
+ ],
15
+ "exclude": [
16
+ "**/*.test.ts",
17
+ "**/*.spec.ts"
18
+ ]
19
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 화면이 다루는 모양.
3
+ *
4
+ * 정본 형식(`FigureSource`)의 타입은 **여기서 다시 정의하지 않는다** —
5
+ * `@hatiolab/figure-model` 에서 가져온다. 두 벌이면 갈리고, 갈리면 화면이 서버가
6
+ * 거절할 것을 만들어 낸다.
7
+ */
8
+ export type { FigureSource, FigureBlueprint, FigurePart, ErrorCode, ViolationCode } from '@hatiolab/figure-model'
9
+
10
+ /** 서버가 돌려주는 Figure 한 벌. `source` 는 저작면에서만 받는다. */
11
+ export interface Figure {
12
+ id: string
13
+ /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */
14
+ type: string
15
+ name: string
16
+ description?: string
17
+ category?: string
18
+ state?: 'draft' | 'released'
19
+ /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */
20
+ score?: number
21
+ triangles?: number
22
+ groups?: number
23
+ thumbnail?: string
24
+ thumbnailUpdatedAt?: string
25
+ updatedAt?: string
26
+ updater?: { id: string; name: string }
27
+ /** `FigureSource` JSON 문자열. 목록 질의에는 실려 오지 않는다. */
28
+ source?: string
29
+ properties?: string
30
+ version?: number
31
+ }
32
+
33
+ export interface FigureListResult {
34
+ items: Figure[]
35
+ total: number
36
+ }
37
+
38
+ /**
39
+ * 새 Figure.
40
+ *
41
+ * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면
42
+ * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.
43
+ */
44
+ export interface NewFigure {
45
+ type: string
46
+ name: string
47
+ description?: string
48
+ category?: string
49
+ source: string
50
+ properties?: string
51
+ state?: 'draft' | 'released'
52
+ thumbnail?: string
53
+ }
54
+
55
+ /**
56
+ * 수정.
57
+ *
58
+ * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 보드가 그 이름으로 타입을
59
+ * 찾으므로, 바꾸면 그 보드가 빈 화면이 된다.
60
+ */
61
+ export interface FigurePatch {
62
+ name?: string
63
+ description?: string
64
+ category?: string
65
+ source?: string
66
+ properties?: string
67
+ state?: 'draft' | 'released'
68
+ thumbnail?: string
69
+ }
70
+
71
+ /** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */
72
+ export interface FigureFinding {
73
+ code: string
74
+ message: string
75
+ at?: string
76
+ }
77
+
78
+ /**
79
+ * 저작 보조가 낸 후보.
80
+ *
81
+ * **정본이 아니다.** 저작자가 보고 받아야 정본이 된다 — 서버는 저장하지 않고
82
+ * 돌려주기만 한다.
83
+ */
84
+ export interface FigureProposal {
85
+ /** 후보 정본. JSON 문자열이다 — 화면이 파싱한다. */
86
+ source: string
87
+ score: number
88
+ grade: string
89
+ triangles: number
90
+ /** 재질 묶음 = 인스턴스 하나가 무는 draw call. */
91
+ groups: number
92
+ /** 형식은 맞으나 정책을 넘은 것. 막지 않는다 — 저작자가 정한다. */
93
+ violations: FigureFinding[]
94
+ /** 몇 번 만에 됐나. 진단용이다. */
95
+ attempts: number
96
+ }
@@ -0,0 +1,226 @@
1
+ import '@material/web/icon/icon.js'
2
+
3
+ import { css, html, LitElement } from 'lit'
4
+ import { customElement, property } from 'lit/decorators.js'
5
+
6
+ import { i18next, localize } from '@operato/i18n'
7
+
8
+ import { capacityFrom } from '../modeller/scene-budget.js'
9
+ import type { Figure } from '../types.js'
10
+
11
+ /**
12
+ * 이 값 아래면 카드에서 색으로 눈에 띄게 한다.
13
+ *
14
+ * **등급 경계가 아니다.** 등급(A~D)은 `figure-model` 의 `scoreOf` 가 정하고 화면은
15
+ * 그것을 베끼지 않는다. 이것은 「눈에 걸리게 할 선」이고, 화면의 판단이다.
16
+ */
17
+ const ATTENTION_BELOW = 50
18
+
19
+ /**
20
+ * Figure 한 장.
21
+ *
22
+ * 카드에 무엇을 실을지가 이 화면의 값이다. 이름과 썸네일만 실으면 저작자는 **무게를
23
+ * 모른 채** 고르게 된다. 삼각형 수와 점수를 함께 실어, 「이걸 100개 놓아도 되나」를
24
+ * 목록에서 판단할 수 있게 한다.
25
+ */
26
+ @customElement('figure-card')
27
+ export class FigureCard extends localize(i18next)(LitElement) {
28
+ static styles = css`
29
+ :host {
30
+ display: flex;
31
+ flex-direction: column;
32
+ overflow: hidden;
33
+ background-color: var(--md-sys-color-surface-container-lowest);
34
+ border: 1px solid var(--md-sys-color-outline-variant);
35
+ border-radius: 6px;
36
+ cursor: pointer;
37
+ }
38
+ :host(:hover) {
39
+ border-color: var(--md-sys-color-primary);
40
+ }
41
+
42
+ div[thumbnail] {
43
+ position: relative;
44
+ aspect-ratio: 4 / 3;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ background-color: var(--md-sys-color-surface-container);
49
+ overflow: hidden;
50
+ }
51
+ div[thumbnail] img {
52
+ width: 100%;
53
+ height: 100%;
54
+ object-fit: contain;
55
+ }
56
+ /* 썸네일이 아직 없는 것을 빈 사각형으로 두지 않는다 — 왜 없는지 말한다 */
57
+ div[thumbnail] span[nothumb] {
58
+ display: flex;
59
+ flex-direction: column;
60
+ align-items: center;
61
+ gap: 4px;
62
+ font: var(--label-font, inherit);
63
+ color: var(--md-sys-color-outline-variant);
64
+ }
65
+ div[thumbnail] span[nothumb] md-icon {
66
+ --md-icon-size: 28px;
67
+ }
68
+
69
+ span[state] {
70
+ position: absolute;
71
+ top: 6px;
72
+ left: 6px;
73
+ padding: 1px 7px;
74
+ font: var(--label-font, inherit);
75
+ font-size: 0.7rem;
76
+ color: var(--md-sys-color-on-secondary-container);
77
+ background-color: var(--md-sys-color-secondary-container);
78
+ border-radius: 6px;
79
+ }
80
+
81
+ button[remove] {
82
+ position: absolute;
83
+ top: 4px;
84
+ right: 4px;
85
+ display: none;
86
+ padding: 3px;
87
+ color: var(--md-sys-color-on-surface);
88
+ background-color: var(--md-sys-color-surface);
89
+ border: 1px solid var(--md-sys-color-outline-variant);
90
+ border-radius: 6px;
91
+ cursor: pointer;
92
+ }
93
+ button[remove] md-icon {
94
+ --md-icon-size: 16px;
95
+ }
96
+ :host(:hover) button[remove] {
97
+ display: block;
98
+ }
99
+
100
+ div[body] {
101
+ display: flex;
102
+ flex-direction: column;
103
+ gap: 2px;
104
+ padding: 8px 10px 10px 10px;
105
+ }
106
+ span[name] {
107
+ font: var(--theme-font, inherit);
108
+ font-size: 0.92rem;
109
+ color: var(--md-sys-color-on-surface);
110
+ overflow: hidden;
111
+ text-overflow: ellipsis;
112
+ white-space: nowrap;
113
+ }
114
+ /* 타입 이름은 저장되는 식별자다 — 표시 이름과 다르게 보이게 둔다 */
115
+ code[type] {
116
+ font-family: var(--mono-font, monospace);
117
+ font-size: 0.72rem;
118
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
119
+ }
120
+
121
+ div[metrics] {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 10px;
125
+ margin-top: 6px;
126
+ font: var(--label-font, inherit);
127
+ font-size: 0.72rem;
128
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
129
+ }
130
+ /* 「몇 개까지」가 카드의 머리다 — 저작면 머리와 같은 말이라야 한다 */
131
+ span[fits] {
132
+ margin-right: auto;
133
+ font-size: 0.82rem;
134
+ font-weight: 700;
135
+ color: var(--md-sys-color-primary);
136
+ }
137
+ span[grade] {
138
+ padding: 0 6px;
139
+ font-weight: 700;
140
+ color: var(--md-sys-color-on-primary-container);
141
+ background-color: var(--md-sys-color-primary-container);
142
+ border-radius: 6px;
143
+ }
144
+ /* 점수가 낮으면 색으로도 말한다 — 숫자만으로는 눈에 안 들어온다 */
145
+ span[grade][low] {
146
+ color: var(--md-sys-color-on-error, #fff);
147
+ background-color: var(--md-sys-color-error);
148
+ }
149
+ span[unmeasured] {
150
+ font-style: italic;
151
+ color: var(--md-sys-color-outline-variant);
152
+ }
153
+ `
154
+
155
+ @property({ type: Object }) figure!: Figure
156
+
157
+ render() {
158
+ const figure = this.figure
159
+ if (!figure) {
160
+ return html``
161
+ }
162
+
163
+ return html`
164
+ <div thumbnail @click=${() => this.dispatchEvent(new CustomEvent('open'))}>
165
+ ${figure.thumbnail
166
+ ? html`<img src=${figure.thumbnail} alt=${figure.name} />`
167
+ : html`
168
+ <span nothumb>
169
+ <md-icon>deployed_code</md-icon>
170
+ ${i18next.t('text.thumbnail-not-made-yet')}
171
+ </span>
172
+ `}
173
+ ${figure.state === 'draft' ? html`<span state>${i18next.t('label.draft')}</span>` : ''}
174
+ <button
175
+ remove
176
+ title=${i18next.t('button.delete')}
177
+ @click=${(e: Event) => {
178
+ e.stopPropagation()
179
+ this.dispatchEvent(new CustomEvent('remove'))
180
+ }}
181
+ >
182
+ <md-icon>delete</md-icon>
183
+ </button>
184
+ </div>
185
+
186
+ <div body @click=${() => this.dispatchEvent(new CustomEvent('open'))}>
187
+ <span name title=${figure.name}>${figure.name}</span>
188
+ <code type>${figure.type}</code>
189
+
190
+ <div metrics>${this.renderMetrics()}</div>
191
+ </div>
192
+ `
193
+ }
194
+
195
+ /**
196
+ * 무게와 점수.
197
+ *
198
+ * 아직 재지 않은 것(정본을 저장한 적이 없는 것)을 **0 으로 보여 주지 않는다.**
199
+ * 0 은 「가볍다」는 뜻이 되어 버린다. 재지 않았다고 말한다.
200
+ */
201
+ private renderMetrics() {
202
+ const { score, triangles, groups } = this.figure
203
+
204
+ if (score == null || triangles == null) {
205
+ return html`<span unmeasured>${i18next.t('text.not-measured-yet')}</span>`
206
+ }
207
+
208
+ // 목록이 저작면과 **같은 말**을 하게 한다. 저작면 머리가 「몇 개까지」이면 카드도
209
+ // 그것이어야 한다 — 같은 형상을 두 화면이 다른 잣대로 말하면 무엇을 고칠지 모른다.
210
+ const capacity = capacityFrom(groups ?? 0, triangles)
211
+
212
+ // 등급 문자(A~D)를 여기서 만들지 않는다. 경계는 `figure-model` 의 `scoreOf` 가
213
+ // 정하고, 그것을 화면이 베끼면 두 벌이 되어 목록과 저작면이 다른 등급을 보여 준다.
214
+ // 목록에는 점수 숫자만 싣는다. 등급이 필요해지면 `figure-model` 이
215
+ // `gradeOf(score)` 를 내보내야 한다 — 그때 그것을 쓴다.
216
+ return html`
217
+ ${capacity
218
+ ? html`<span fits title=${i18next.t('text.reuse-score-explained')}>
219
+ ${i18next.t('text.fits-n-instances', { n: capacity.instances.toLocaleString() })}
220
+ </span>`
221
+ : ''}
222
+ <span grade ?low=${score < ATTENTION_BELOW}>${Math.round(score)}</span>
223
+ <span>${i18next.t('text.triangle-count', { count: triangles })}</span>
224
+ `
225
+ }
226
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 클라이언트 부트스트랩.
3
+ *
4
+ * 지금은 등록할 것이 없다. 그래도 이 파일을 두는 이유는 `things-factory.config.js` 가
5
+ * `bootstrap` 을 가리키기 때문이고, 데이터 그리드용 편집기·렌더러나 속성 편집기를
6
+ * 붙이게 되면 여기가 그 자리다(`board-ui/client/bootstrap.ts` 가 그 예다).
7
+ *
8
+ * ## 메뉴는 여기서 걸지 않는다
9
+ *
10
+ * things-factory 의 메뉴는 **`menu-base` 의 DB 레코드**다. 코드가 메뉴를 더하는 길은
11
+ * 없다 — 화면은 `things-factory.config.js` 의 `routes` 로 등록되고, 그 화면을 메뉴에
12
+ * 올리는 것은 운영자가 메뉴를 만들어 `figure-list` 를 가리키게 하는 일이다.
13
+ */
14
+ export default function bootstrap(): void;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 클라이언트 부트스트랩.
3
+ *
4
+ * 지금은 등록할 것이 없다. 그래도 이 파일을 두는 이유는 `things-factory.config.js` 가
5
+ * `bootstrap` 을 가리키기 때문이고, 데이터 그리드용 편집기·렌더러나 속성 편집기를
6
+ * 붙이게 되면 여기가 그 자리다(`board-ui/client/bootstrap.ts` 가 그 예다).
7
+ *
8
+ * ## 메뉴는 여기서 걸지 않는다
9
+ *
10
+ * things-factory 의 메뉴는 **`menu-base` 의 DB 레코드**다. 코드가 메뉴를 더하는 길은
11
+ * 없다 — 화면은 `things-factory.config.js` 의 `routes` 로 등록되고, 그 화면을 메뉴에
12
+ * 올리는 것은 운영자가 메뉴를 만들어 `figure-list` 를 가리키게 하는 일이다.
13
+ */
14
+ export default function bootstrap() {
15
+ // 등록할 것이 생기면 여기에 둔다.
16
+ }
17
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,qBAAqB;AACvB,CAAC","sourcesContent":["/**\n * 클라이언트 부트스트랩.\n *\n * 지금은 등록할 것이 없다. 그래도 이 파일을 두는 이유는 `things-factory.config.js` 가\n * `bootstrap` 을 가리키기 때문이고, 데이터 그리드용 편집기·렌더러나 속성 편집기를\n * 붙이게 되면 여기가 그 자리다(`board-ui/client/bootstrap.ts` 가 그 예다).\n *\n * ## 메뉴는 여기서 걸지 않는다\n *\n * things-factory 의 메뉴는 **`menu-base` 의 DB 레코드**다. 코드가 메뉴를 더하는 길은\n * 없다 — 화면은 `things-factory.config.js` 의 `routes` 로 등록되고, 그 화면을 메뉴에\n * 올리는 것은 운영자가 메뉴를 만들어 `figure-list` 를 가리키게 하는 일이다.\n */\nexport default function bootstrap() {\n // 등록할 것이 생기면 여기에 둔다.\n}\n"]}
@@ -0,0 +1,41 @@
1
+ import type { Figure, FigureFinding, FigureListResult, FigurePatch, FigureProposal, NewFigure } from '../types.js';
2
+ export declare function fetchFigureList(params: {
3
+ page?: number;
4
+ limit?: number;
5
+ search?: string;
6
+ category?: string;
7
+ state?: string;
8
+ }): Promise<FigureListResult>;
9
+ /** 저작면에 필요한 전부 — `source` 를 포함한다. */
10
+ export declare function fetchFigure(id: string): Promise<Figure>;
11
+ /**
12
+ * 이미 쓰이고 있는 타입 이름.
13
+ *
14
+ * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러
15
+ * 거절당하고 나서 아는 것은 늦다.
16
+ */
17
+ export declare function fetchFigureTypeNames(): Promise<string[]>;
18
+ export interface SaveResult {
19
+ figure: Figure;
20
+ /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */
21
+ violations: FigureFinding[];
22
+ }
23
+ export declare function createFigure(figure: NewFigure): Promise<SaveResult>;
24
+ export declare function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult>;
25
+ export declare function deleteFigure(id: string): Promise<boolean>;
26
+ /**
27
+ * 저작 보조에게 후보를 시킨다.
28
+ *
29
+ * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고
30
+ * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을
31
+ * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.
32
+ *
33
+ * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.
34
+ */
35
+ export declare function proposeFigure(request: {
36
+ prompt: string;
37
+ base?: string;
38
+ type?: string;
39
+ palette: string[];
40
+ image?: File;
41
+ }): Promise<FigureProposal>;