@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,280 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import '../viewparts/figure-card.js';
4
+ import { css, html } from 'lit';
5
+ import { customElement, state } from 'lit/decorators.js';
6
+ import { i18next, localize } from '@operato/i18n';
7
+ import { navigate, PageView } from '@operato/shell';
8
+ import { ScrollbarStyles } from '@operato/styles';
9
+ import { deleteFigure, fetchFigureList } from '../graphql/index.js';
10
+ const FigureListPageBase = localize(i18next)(PageView);
11
+ /**
12
+ * Figure 목록.
13
+ *
14
+ * 이 화면은 저작면으로 가는 문이다. 여기서 오래 머물 화면이 아니므로 꾸미지 않는다.
15
+ * 대신 **저작자가 목록에서 판단할 수 있어야 하는 것**은 카드에 실는다 — 무게(삼각형
16
+ * 수)와 재활용 점수다. 그것을 저작면에 들어가서야 알면 늦다.
17
+ */
18
+ let FigureListPage = class FigureListPage extends FigureListPageBase {
19
+ constructor() {
20
+ super(...arguments);
21
+ this.figures = [];
22
+ this.total = 0;
23
+ this.search = '';
24
+ this.stateFilter = '';
25
+ this.loaded = false;
26
+ }
27
+ static { this.styles = [
28
+ ScrollbarStyles,
29
+ css `
30
+ :host {
31
+ display: flex;
32
+ flex-direction: column;
33
+ overflow: hidden;
34
+ background-color: var(--md-sys-color-surface);
35
+ color: var(--md-sys-color-on-surface);
36
+ }
37
+
38
+ /* 걸러 보기 줄 */
39
+ div[filters] {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: var(--spacing-medium, 8px);
43
+ padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);
44
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
45
+ }
46
+
47
+ input[search] {
48
+ flex: 1;
49
+ min-width: 0;
50
+ max-width: 320px;
51
+ padding: 6px 10px;
52
+ font: var(--input-field-font, inherit);
53
+ color: var(--md-sys-color-on-surface);
54
+ background-color: var(--md-sys-color-surface-container-lowest);
55
+ border: 1px solid var(--md-sys-color-outline-variant);
56
+ /* 명령 요소의 모서리는 테마가 정한다 — 알약으로 만들지 않는다 */
57
+ border-radius: 6px;
58
+ }
59
+
60
+ button[state] {
61
+ padding: 5px 12px;
62
+ font: var(--label-font, inherit);
63
+ color: var(--md-sys-color-on-surface);
64
+ background: none;
65
+ border: 1px solid var(--md-sys-color-outline-variant);
66
+ border-radius: 6px;
67
+ cursor: pointer;
68
+ }
69
+ button[state][selected] {
70
+ color: var(--md-sys-color-on-secondary-container);
71
+ background-color: var(--md-sys-color-secondary-container);
72
+ border-color: transparent;
73
+ }
74
+
75
+ span[count] {
76
+ margin-left: auto;
77
+ font: var(--label-font, inherit);
78
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
79
+ }
80
+
81
+ /* 카드 격자 */
82
+ div[cards] {
83
+ flex: 1;
84
+ overflow-y: auto;
85
+ display: grid;
86
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
87
+ gap: var(--spacing-medium, 8px);
88
+ padding: var(--spacing-large, 12px);
89
+ align-content: start;
90
+ }
91
+
92
+ /* ── 아무것도 없을 때 ──────────────────────────────────────────────
93
+ 이 자리가 이 화면에서 가장 중요하다. 처음 온 사람은 여기서 이 도구가
94
+ 무엇을 만드는 것인지 알게 된다. 「데이터 없음」으로 끝내지 않는다. */
95
+ div[empty] {
96
+ flex: 1;
97
+ display: flex;
98
+ flex-direction: column;
99
+ align-items: center;
100
+ justify-content: center;
101
+ gap: var(--spacing-medium, 8px);
102
+ padding: var(--spacing-large, 12px);
103
+ text-align: center;
104
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
105
+ }
106
+ div[empty] md-icon {
107
+ --md-icon-size: 48px;
108
+ color: var(--md-sys-color-outline-variant);
109
+ }
110
+ div[empty] h2 {
111
+ margin: 0;
112
+ font: var(--theme-font, inherit);
113
+ font-size: var(--ui-t-title, 15px);
114
+ color: var(--md-sys-color-on-surface);
115
+ }
116
+ div[empty] p {
117
+ margin: 0;
118
+ max-width: 34em;
119
+ line-height: 1.6;
120
+ }
121
+ div[empty] button {
122
+ margin-top: var(--spacing-medium, 8px);
123
+ padding: 8px 18px;
124
+ font: var(--label-font, inherit);
125
+ color: var(--md-sys-color-on-primary, #fff);
126
+ background-color: var(--md-sys-color-primary);
127
+ border: none;
128
+ border-radius: 6px;
129
+ cursor: pointer;
130
+ }
131
+ `
132
+ ]; }
133
+ get context() {
134
+ return {
135
+ title: i18next.t('title.figures'),
136
+ actions: [
137
+ {
138
+ title: i18next.t('button.new-figure'),
139
+ action: () => this.openModeller()
140
+ }
141
+ ]
142
+ };
143
+ }
144
+ render() {
145
+ return html `
146
+ <div filters>
147
+ <input
148
+ search
149
+ type="search"
150
+ .value=${this.search}
151
+ placeholder=${i18next.t('text.search-figure-by-name-or-type')}
152
+ @change=${(e) => {
153
+ this.search = e.target.value;
154
+ this.reload();
155
+ }}
156
+ />
157
+ ${[
158
+ { value: '', label: i18next.t('label.all') },
159
+ { value: 'draft', label: i18next.t('label.draft') },
160
+ { value: 'released', label: i18next.t('label.released') }
161
+ ].map(option => html `
162
+ <button
163
+ state
164
+ ?selected=${this.stateFilter === option.value}
165
+ @click=${() => {
166
+ this.stateFilter = option.value;
167
+ this.reload();
168
+ }}
169
+ >
170
+ ${option.label}
171
+ </button>
172
+ `)}
173
+ ${this.loaded ? html `<span count>${i18next.t('text.figure-count', { count: this.total })}</span>` : ''}
174
+ </div>
175
+
176
+ ${!this.loaded
177
+ ? html ``
178
+ : this.figures.length === 0
179
+ ? this.renderEmpty()
180
+ : html `
181
+ <div cards>
182
+ ${this.figures.map(figure => html `
183
+ <figure-card
184
+ .figure=${figure}
185
+ @open=${() => this.openModeller(figure.id)}
186
+ @remove=${() => this.removeFigure(figure)}
187
+ ></figure-card>
188
+ `)}
189
+ </div>
190
+ `}
191
+ `;
192
+ }
193
+ /**
194
+ * 빈 화면.
195
+ *
196
+ * 걸러 보기 때문에 비었는지, 아직 만든 것이 없어서 비었는지를 **가른다.** 같은
197
+ * 문구를 쓰면 사용자가 검색어를 지워 볼 생각을 못 한다.
198
+ */
199
+ renderEmpty() {
200
+ const filtered = !!this.search || !!this.stateFilter;
201
+ if (filtered) {
202
+ return html `
203
+ <div empty>
204
+ <md-icon>search_off</md-icon>
205
+ <h2>${i18next.t('text.no-figure-matches-the-filter')}</h2>
206
+ <p>${i18next.t('text.try-clearing-the-search-or-state-filter')}</p>
207
+ <button
208
+ @click=${() => {
209
+ this.search = '';
210
+ this.stateFilter = '';
211
+ this.reload();
212
+ }}
213
+ >
214
+ ${i18next.t('button.clear-filters')}
215
+ </button>
216
+ </div>
217
+ `;
218
+ }
219
+ return html `
220
+ <div empty>
221
+ <md-icon>deployed_code</md-icon>
222
+ <h2>${i18next.t('text.no-figure-yet')}</h2>
223
+ <p>${i18next.t('text.what-a-figure-is')}</p>
224
+ <button @click=${() => this.openModeller()}>${i18next.t('button.new-figure')}</button>
225
+ </div>
226
+ `;
227
+ }
228
+ async pageUpdated(changes, lifecycle) {
229
+ if (this.active) {
230
+ await this.reload();
231
+ }
232
+ }
233
+ async reload() {
234
+ const { items, total } = await fetchFigureList({
235
+ search: this.search || undefined,
236
+ state: this.stateFilter || undefined
237
+ });
238
+ this.figures = items;
239
+ this.total = total;
240
+ this.loaded = true;
241
+ }
242
+ openModeller(id) {
243
+ navigate(id ? `figure-modeller/${id}` : 'figure-modeller');
244
+ }
245
+ /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */
246
+ async removeFigure(figure) {
247
+ // 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다
248
+ const message = i18next.t('text.delete-figure-confirm', { name: figure.name });
249
+ if (!confirm(message)) {
250
+ return;
251
+ }
252
+ await deleteFigure(figure.id);
253
+ await this.reload();
254
+ }
255
+ };
256
+ __decorate([
257
+ state(),
258
+ __metadata("design:type", Array)
259
+ ], FigureListPage.prototype, "figures", void 0);
260
+ __decorate([
261
+ state(),
262
+ __metadata("design:type", Object)
263
+ ], FigureListPage.prototype, "total", void 0);
264
+ __decorate([
265
+ state(),
266
+ __metadata("design:type", Object)
267
+ ], FigureListPage.prototype, "search", void 0);
268
+ __decorate([
269
+ state(),
270
+ __metadata("design:type", Object)
271
+ ], FigureListPage.prototype, "stateFilter", void 0);
272
+ __decorate([
273
+ state(),
274
+ __metadata("design:type", Object)
275
+ ], FigureListPage.prototype, "loaded", void 0);
276
+ FigureListPage = __decorate([
277
+ customElement('figure-list-page')
278
+ ], FigureListPage);
279
+ export { FigureListPage };
280
+ //# sourceMappingURL=figure-list-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"figure-list-page.js","sourceRoot":"","sources":["../../client/pages/figure-list-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../viewparts/figure-card.js'\n\nimport { css, html } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { navigate, PageView } from '@operato/shell'\nimport { ScrollbarStyles } from '@operato/styles'\n\nimport { deleteFigure, fetchFigureList } from '../graphql/index.js'\nimport type { Figure } from '../types.js'\n\nconst FigureListPageBase = localize(i18next)(PageView) as typeof PageView\n\n/**\n * Figure 목록.\n *\n * 이 화면은 저작면으로 가는 문이다. 여기서 오래 머물 화면이 아니므로 꾸미지 않는다.\n * 대신 **저작자가 목록에서 판단할 수 있어야 하는 것**은 카드에 실는다 — 무게(삼각형\n * 수)와 재활용 점수다. 그것을 저작면에 들어가서야 알면 늦다.\n */\n@customElement('figure-list-page')\nexport class FigureListPage extends FigureListPageBase {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n\n /* 걸러 보기 줄 */\n div[filters] {\n display: flex;\n align-items: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-medium, 8px) var(--spacing-large, 12px);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n\n input[search] {\n flex: 1;\n min-width: 0;\n max-width: 320px;\n padding: 6px 10px;\n font: var(--input-field-font, inherit);\n color: var(--md-sys-color-on-surface);\n background-color: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n /* 명령 요소의 모서리는 테마가 정한다 — 알약으로 만들지 않는다 */\n border-radius: 6px;\n }\n\n button[state] {\n padding: 5px 12px;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n cursor: pointer;\n }\n button[state][selected] {\n color: var(--md-sys-color-on-secondary-container);\n background-color: var(--md-sys-color-secondary-container);\n border-color: transparent;\n }\n\n span[count] {\n margin-left: auto;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n\n /* 카드 격자 */\n div[cards] {\n flex: 1;\n overflow-y: auto;\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n align-content: start;\n }\n\n /* ── 아무것도 없을 때 ──────────────────────────────────────────────\n 이 자리가 이 화면에서 가장 중요하다. 처음 온 사람은 여기서 이 도구가\n 무엇을 만드는 것인지 알게 된다. 「데이터 없음」으로 끝내지 않는다. */\n div[empty] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--spacing-medium, 8px);\n padding: var(--spacing-large, 12px);\n text-align: center;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n }\n div[empty] md-icon {\n --md-icon-size: 48px;\n color: var(--md-sys-color-outline-variant);\n }\n div[empty] h2 {\n margin: 0;\n font: var(--theme-font, inherit);\n font-size: var(--ui-t-title, 15px);\n color: var(--md-sys-color-on-surface);\n }\n div[empty] p {\n margin: 0;\n max-width: 34em;\n line-height: 1.6;\n }\n div[empty] button {\n margin-top: var(--spacing-medium, 8px);\n padding: 8px 18px;\n font: var(--label-font, inherit);\n color: var(--md-sys-color-on-primary, #fff);\n background-color: var(--md-sys-color-primary);\n border: none;\n border-radius: 6px;\n cursor: pointer;\n }\n `\n ]\n\n @state() private figures: Figure[] = []\n @state() private total = 0\n @state() private search = ''\n @state() private stateFilter = ''\n @state() private loaded = false\n\n get context() {\n return {\n title: i18next.t('title.figures'),\n actions: [\n {\n title: i18next.t('button.new-figure'),\n action: () => this.openModeller()\n }\n ]\n }\n }\n\n render() {\n return html`\n <div filters>\n <input\n search\n type=\"search\"\n .value=${this.search}\n placeholder=${i18next.t('text.search-figure-by-name-or-type')}\n @change=${(e: Event) => {\n this.search = (e.target as HTMLInputElement).value\n this.reload()\n }}\n />\n ${[\n { value: '', label: i18next.t('label.all') },\n { value: 'draft', label: i18next.t('label.draft') },\n { value: 'released', label: i18next.t('label.released') }\n ].map(\n option => html`\n <button\n state\n ?selected=${this.stateFilter === option.value}\n @click=${() => {\n this.stateFilter = option.value\n this.reload()\n }}\n >\n ${option.label}\n </button>\n `\n )}\n ${this.loaded ? html`<span count>${i18next.t('text.figure-count', { count: this.total })}</span>` : ''}\n </div>\n\n ${!this.loaded\n ? html``\n : this.figures.length === 0\n ? this.renderEmpty()\n : html`\n <div cards>\n ${this.figures.map(\n figure => html`\n <figure-card\n .figure=${figure}\n @open=${() => this.openModeller(figure.id)}\n @remove=${() => this.removeFigure(figure)}\n ></figure-card>\n `\n )}\n </div>\n `}\n `\n }\n\n /**\n * 빈 화면.\n *\n * 걸러 보기 때문에 비었는지, 아직 만든 것이 없어서 비었는지를 **가른다.** 같은\n * 문구를 쓰면 사용자가 검색어를 지워 볼 생각을 못 한다.\n */\n private renderEmpty() {\n const filtered = !!this.search || !!this.stateFilter\n\n if (filtered) {\n return html`\n <div empty>\n <md-icon>search_off</md-icon>\n <h2>${i18next.t('text.no-figure-matches-the-filter')}</h2>\n <p>${i18next.t('text.try-clearing-the-search-or-state-filter')}</p>\n <button\n @click=${() => {\n this.search = ''\n this.stateFilter = ''\n this.reload()\n }}\n >\n ${i18next.t('button.clear-filters')}\n </button>\n </div>\n `\n }\n\n return html`\n <div empty>\n <md-icon>deployed_code</md-icon>\n <h2>${i18next.t('text.no-figure-yet')}</h2>\n <p>${i18next.t('text.what-a-figure-is')}</p>\n <button @click=${() => this.openModeller()}>${i18next.t('button.new-figure')}</button>\n </div>\n `\n }\n\n async pageUpdated(changes: Record<string, unknown>, lifecycle: Record<string, unknown>) {\n if (this.active) {\n await this.reload()\n }\n }\n\n private async reload() {\n const { items, total } = await fetchFigureList({\n search: this.search || undefined,\n state: this.stateFilter || undefined\n })\n\n this.figures = items\n this.total = total\n this.loaded = true\n }\n\n private openModeller(id?: string) {\n navigate(id ? `figure-modeller/${id}` : 'figure-modeller')\n }\n\n /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */\n private async removeFigure(figure: Figure) {\n // 되돌릴 수 없는 조작이므로 무엇이 사라지는지 이름으로 확인한다\n const message = i18next.t('text.delete-figure-confirm', { name: figure.name })\n if (!confirm(message)) {\n return\n }\n\n await deleteFigure(figure.id)\n await this.reload()\n }\n}\n"]}
@@ -0,0 +1,219 @@
1
+ import '@material/web/icon/icon.js';
2
+ import '../modeller/figure-side.js';
3
+ import '../modeller/figure-ask.js';
4
+ import '../modeller/figure-canvas.js';
5
+ import '../modeller/figure-preview.js';
6
+ import '../modeller/figure-parts.js';
7
+ import '../modeller/figure-inspector.js';
8
+ import { PageView } from '@operato/shell';
9
+ declare const FigureModellerPageBase: typeof PageView;
10
+ /**
11
+ * Figure 저작면.
12
+ *
13
+ * ## 세 칸이다
14
+ *
15
+ * 왼쪽 도형 팔레트 · 부품 목록 · 속성
16
+ * 가운데 미리보기 — `FigureInstance` 를 실제로 세운다
17
+ * 오른쪽 판단 — 늘 떠 있다
18
+ *
19
+ * 미리보기가 씬 위에서 실제로 동작하는 것이 이 도구의 전제다. **편집한 것이 곧 실제 결과물**이라야
20
+ * "모델러에서는 되는데 보드에서는 다르다"가 생기지 않는다.
21
+ *
22
+ * ## 편집 데이터는 씬 모델 하나뿐이다
23
+ *
24
+ * 이 페이지가 갖는 것은 보드 하나와 부품 목록 — 미리보기에 그대로 세워지는 씬 모델이다.
25
+ * 저장 형식(`FigureSource`)으로 바꾸는 것은 **경계에서만** 한다: 저장할 때, 판단할 때,
26
+ * AI 후보와 비교할 때. 그 변환은 `figure-source.ts` 한 곳에서만 일어난다.
27
+ *
28
+ * 팔레트도 속성 편집기도 이 데이터를 직접 수정하지 않는다. 무엇을 하겠다는 것만 알리고,
29
+ * 바꾸는 것은 이 페이지다. 두 곳에서 고치면 실행 취소도 저장도 어느 쪽이 맞는지 알 수
30
+ * 없게 된다.
31
+ */
32
+ export declare class FigureModellerPage extends FigureModellerPageBase {
33
+ static styles: import("lit").CSSResult;
34
+ /** 편집 중인 Figure. 새로 만드는 중이면 `id` 가 없다. */
35
+ private figure?;
36
+ /** 편집 데이터 — 미리보기에 그대로 세워지는 씬 모델이다. */
37
+ private board?;
38
+ private parts;
39
+ private selected;
40
+ private view;
41
+ /**
42
+ * 가운데 칸이 무엇을 보이나.
43
+ *
44
+ * 편집 캔버스는 늘 기준 크기다. 그런데 이 형식의 값 절반은 크기가 달라졌을 때 어떻게
45
+ * 반응하는가에 있다. 그것을 볼 자리가 없었다.
46
+ */
47
+ private mode;
48
+ /**
49
+ * 어느 칸이 접혀 있나.
50
+ *
51
+ * 통째로 갈아 끼운다 — 안쪽 값만 바꾸면 Lit 이 같은 객체로 보고 다시 그리지 않는다.
52
+ */
53
+ private collapsed;
54
+ private dirty;
55
+ private saving;
56
+ private violations;
57
+ /** 저작 보조가 낸 후보. 받기 전에는 원본 데이터가 아니다. */
58
+ private proposal?;
59
+ /** 그 후보의 셈. 무엇을 받는 것인지 숫자로도 보여 준다. */
60
+ private proposalOf?;
61
+ /** 받기로 고른 변경들. 처음에는 전부 골라 둔다. */
62
+ private picked;
63
+ /**
64
+ * 저장 형식으로 변환한 것. 씬 모델이 바뀔 때만 다시 만든다.
65
+ *
66
+ * 저장·판단·AI 후보 비교가 이것을 쓴다. 렌더링할 때마다 새로 만들면 판단 패널이 매번
67
+ * 다시 계산하므로, 변환은 `willUpdate` 에서 한 번만 한다.
68
+ */
69
+ private folded?;
70
+ willUpdate(changed: Map<string, unknown>): void;
71
+ get context(): {
72
+ title: string;
73
+ };
74
+ render(): import("lit-html").TemplateResult<1>;
75
+ /**
76
+ * 가운데 칸 위의 띠 — 무엇을 보고 있나와, 양쪽 칸을 접는 손잡이.
77
+ *
78
+ * ## 무엇을 보고 있나
79
+ *
80
+ * 편집과 미리보기는 같은 자리를 놓고 다투지 않는다. 둘 다 넓이가 필요하고, 동시에
81
+ * 봐야 할 일이 없다. 부품을 고치는 동안에는 편집만 보면 되고, 다 고친 뒤에 보드에서
82
+ * 어떻게 되는지 보면 된다.
83
+ *
84
+ * 후보가 와 있을 때는 그 두 단추를 내지 않는다. 그때는 가운데가 이미 둘로 나뉘어
85
+ * 있다.
86
+ *
87
+ * ## 띠는 늘 있다
88
+ *
89
+ * 손잡이가 여기 있기 때문이다. 후보가 왔다고 띠를 통째로 감추면, 칸을 접어 둔 채로
90
+ * 후보를 받으면 되돌릴 방법이 없어진다.
91
+ */
92
+ private renderBar;
93
+ /**
94
+ * 한쪽 칸을 접고 펴는 손잡이.
95
+ *
96
+ * 화살표는 **누르면 무엇이 일어나는지**를 가리킨다 — 펴져 있으면 칸이 사라질 쪽,
97
+ * 접혀 있으면 칸이 돌아올 쪽. 지금 상태를 가리키게 하면 반대로 읽힌다.
98
+ */
99
+ private renderCollapse;
100
+ private toggleCollapse;
101
+ /**
102
+ * AI 후보를 나란히 세운다.
103
+ *
104
+ * 후보는 저장 형식으로 온다. 미리보기는 씬 모델을 받으므로 여기서 한 번 변환한다 —
105
+ * 후보를 받아들이기 전까지는 편집 데이터가 아니므로 상태로 두지 않는다.
106
+ */
107
+ private renderProposed;
108
+ /**
109
+ * 무엇을 받을지 고른다.
110
+ *
111
+ * 통째로만 받게 두면 「몸통은 좋은데 모터 자리가 틀렸다」에 답할 수 없다 — 그게
112
+ * 가장 흔한 반응이다. 변경 하나가 칩 하나이고, 처음에는 전부 골라 둔다. 통째로
113
+ * 받는 것이 보통의 길이므로 그것이 한 번 누르기여야 한다.
114
+ *
115
+ * 고른 조합을 **받기 전에 재 본다.** 형식을 어기면 못 누르게 하고, 정책을 넘으면
116
+ * 알린다. 골라 받으면 후보를 낼 때와 다른 조합이 되므로 후보의 점수를 그대로
117
+ * 믿을 수 없다 — 예를 들어 등급은 안 받고 부품만 받으면 부품 수 한도를 넘는다.
118
+ *
119
+ * 형식 오류가 실제로 나는 조합은 아직 보지 못했다. 그래도 두는 것은 값이 거의
120
+ * 안 들고, 우리가 생각 못 한 조합을 막아 주기 때문이다.
121
+ */
122
+ private renderDecision;
123
+ /** 변경 하나. 무엇을 하겠다는 것인지 부호로 먼저 말한다. */
124
+ private renderChange;
125
+ private toggle;
126
+ /**
127
+ * 후보가 왔다.
128
+ *
129
+ * 원본 데이터에 넣지 않는다. 나란히 두고 저작자가 고른다. AI 가 원본 데이터에 직접 쓰면
130
+ * 무엇이 자기 것이고 무엇이 기계 것인지 모르게 된다.
131
+ */
132
+ private receive;
133
+ /**
134
+ * 고른 변경을 받아들인다.
135
+ *
136
+ * 후보 비교는 저장 형식으로 한다 — 부품 이름으로 짝짓고 기준 치수·등급까지 함께 봐야
137
+ * 하므로 그쪽이 자연스럽다. 받아들인 결과는 다시 씬 모델로 되돌려 편집을 이어간다.
138
+ */
139
+ private take;
140
+ private discard;
141
+ /**
142
+ * 저장할 수 있나.
143
+ *
144
+ * 형식을 어긴 원본 데이터로는 저장을 누를 수 없게 한다. 눌렀다가 서버가 받아주지 않는 것보다,
145
+ * 누를 수 없는 이유가 판단 패널에 떠 있는 것이 낫다.
146
+ */
147
+ private get canSave();
148
+ pageUpdated(changes: Record<string, unknown>, lifecycle: {
149
+ resourceId?: string;
150
+ }): Promise<void>;
151
+ private load;
152
+ private startNew;
153
+ private rename;
154
+ /**
155
+ * 부품 목록을 갈아 끼운다.
156
+ *
157
+ * 편집 규칙은 `part-edits` 가 갖는다 — 화면과 테스트가 같은 것을 쓰게 하려고 떼어
158
+ * 놓았다. 여기서는 결과를 담고 선택 인덱스를 정리한다.
159
+ */
160
+ private put;
161
+ private addPart;
162
+ private removePart;
163
+ private duplicatePart;
164
+ private movePart;
165
+ private updatePart;
166
+ /**
167
+ * 디테일 등급을 바꾼다.
168
+ *
169
+ * 이 값은 부품 수 한도를 정하고 성숙도의 한 축이 된다. 부품보다 위에 있는 값이라
170
+ * 보드에 담는다.
171
+ *
172
+ * 한도를 내려서 이미 넣은 부품 수보다 작아질 수 있다. 그때 부품을 지우지 않는다.
173
+ * 검증이 위반으로 알리고, 성숙도가 등급에 안 맞는다고 말한다. 저작자가 무엇을 뺄지
174
+ * 정할 일이지 도구가 정할 일이 아니다.
175
+ */
176
+ private changeDetailLevel;
177
+ /**
178
+ * 부품을 잠깐 가린다.
179
+ *
180
+ * 씬이 이미 갖고 있는 상태를 켜고 끄는 것뿐이다 — 편집 모드에서는 faint 로 보이고
181
+ * 뷰 모드에서는 안 그려진다. 저장 형식에는 실리지 않으므로 다시 열면 전부 보인다.
182
+ *
183
+ * `dirty` 로 치지 않는다. 저장될 것이 하나도 바뀌지 않았는데 "저장하지 않은 변경"이
184
+ * 뜨면, 저장 버튼이 무엇을 저장하겠다는 것인지 알 수 없다.
185
+ */
186
+ private toggleHidden;
187
+ /**
188
+ * 한꺼번에 감추거나 되돌린다.
189
+ *
190
+ * 부품 스물짜리에서 안쪽 하나를 보려고 열아홉을 누르던 것을 없앤다. 한 번에 바꾸므로
191
+ * 목록을 한 번만 갈아 끼운다 — 하나씩 스무 번 갈면 미리보기가 스무 번 다시 선다.
192
+ */
193
+ private setAllHidden;
194
+ /**
195
+ * 미리보기에서 기즈모로 옮기거나 2D 에서 끌었다.
196
+ *
197
+ * 알려 오는 것은 부품 **이름**이다. 목록의 몇 번째인지는 여기서 찾는다 — 이벤트가
198
+ * 오는 사이에 목록이 바뀌었으면 찾지 못하고, 그때는 아무것도 하지 않는다.
199
+ */
200
+ private partChanged;
201
+ /**
202
+ * 정밀 배치 도구.
203
+ *
204
+ * 규칙은 `part-edits` 가 갖는다. 여기서는 어느 것을 부를지만 정한다.
205
+ */
206
+ private runTool;
207
+ /**
208
+ * 캔버스에서 부품을 선택했다.
209
+ *
210
+ * 캔버스가 알리는 것은 **이름**이다. 목록의 몇 번째인지는 여기서 찾는다 — 알림이
211
+ * 오는 사이에 목록이 바뀌었으면 못 찾고, 그때는 고른 것을 바꾸지 않는다.
212
+ * 없는 자리를 고르면 속성 칸이 빈 채로 열려 「사라졌나」로 읽힌다.
213
+ */
214
+ /** 지금 고른 부품 이름. 캔버스가 인덱스가 아니라 이름을 받는다. */
215
+ private get pickedName();
216
+ private pickPart;
217
+ private save;
218
+ }
219
+ export {};