@things-factory/figure-ui 10.1.5 → 10.1.7

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 (79) hide show
  1. package/client/graphql/index.ts +36 -3
  2. package/client/modeller/figure-animations.ts +712 -0
  3. package/client/modeller/figure-ask.ts +5 -5
  4. package/client/modeller/figure-canvas.ts +1 -1
  5. package/client/modeller/figure-capabilities.ts +372 -0
  6. package/client/modeller/figure-history-panel.ts +171 -18
  7. package/client/modeller/figure-inspector.ts +439 -76
  8. package/client/modeller/figure-parts.ts +15 -15
  9. package/client/modeller/figure-preview.ts +2 -2
  10. package/client/modeller/figure-report.ts +102 -37
  11. package/client/modeller/figure-settings.ts +20 -20
  12. package/client/modeller/figure-side.ts +91 -15
  13. package/client/modeller/figure-source.test.ts +15 -1
  14. package/client/modeller/figure-source.ts +31 -15
  15. package/client/modeller/figure-thumbnail.ts +49 -4
  16. package/client/modeller/maturity.ts +10 -10
  17. package/client/pages/figure-list-page.ts +204 -66
  18. package/client/pages/figure-modeller-page.ts +158 -42
  19. package/client/types.ts +31 -0
  20. package/client/viewparts/figure-thumb.ts +250 -0
  21. package/dist-client/graphql/index.d.ts +8 -1
  22. package/dist-client/graphql/index.js +33 -3
  23. package/dist-client/graphql/index.js.map +1 -1
  24. package/dist-client/modeller/figure-animations.d.ts +93 -0
  25. package/dist-client/modeller/figure-animations.js +655 -0
  26. package/dist-client/modeller/figure-animations.js.map +1 -0
  27. package/dist-client/modeller/figure-ask.js +5 -5
  28. package/dist-client/modeller/figure-ask.js.map +1 -1
  29. package/dist-client/modeller/figure-canvas.js +1 -1
  30. package/dist-client/modeller/figure-canvas.js.map +1 -1
  31. package/dist-client/modeller/figure-capabilities.d.ts +42 -0
  32. package/dist-client/modeller/figure-capabilities.js +344 -0
  33. package/dist-client/modeller/figure-capabilities.js.map +1 -0
  34. package/dist-client/modeller/figure-history-panel.d.ts +15 -1
  35. package/dist-client/modeller/figure-history-panel.js +165 -18
  36. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  37. package/dist-client/modeller/figure-inspector.d.ts +98 -0
  38. package/dist-client/modeller/figure-inspector.js +425 -75
  39. package/dist-client/modeller/figure-inspector.js.map +1 -1
  40. package/dist-client/modeller/figure-parts.js +15 -15
  41. package/dist-client/modeller/figure-parts.js.map +1 -1
  42. package/dist-client/modeller/figure-preview.js +2 -2
  43. package/dist-client/modeller/figure-preview.js.map +1 -1
  44. package/dist-client/modeller/figure-report.d.ts +18 -1
  45. package/dist-client/modeller/figure-report.js +105 -37
  46. package/dist-client/modeller/figure-report.js.map +1 -1
  47. package/dist-client/modeller/figure-settings.js +19 -19
  48. package/dist-client/modeller/figure-settings.js.map +1 -1
  49. package/dist-client/modeller/figure-side.d.ts +24 -0
  50. package/dist-client/modeller/figure-side.js +94 -14
  51. package/dist-client/modeller/figure-side.js.map +1 -1
  52. package/dist-client/modeller/figure-source.d.ts +15 -8
  53. package/dist-client/modeller/figure-source.js +16 -7
  54. package/dist-client/modeller/figure-source.js.map +1 -1
  55. package/dist-client/modeller/figure-thumbnail.js +40 -4
  56. package/dist-client/modeller/figure-thumbnail.js.map +1 -1
  57. package/dist-client/modeller/maturity.js +10 -10
  58. package/dist-client/modeller/maturity.js.map +1 -1
  59. package/dist-client/pages/figure-list-page.d.ts +11 -4
  60. package/dist-client/pages/figure-list-page.js +201 -63
  61. package/dist-client/pages/figure-list-page.js.map +1 -1
  62. package/dist-client/pages/figure-modeller-page.d.ts +33 -0
  63. package/dist-client/pages/figure-modeller-page.js +156 -42
  64. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  65. package/dist-client/tsconfig.tsbuildinfo +1 -1
  66. package/dist-client/types.d.ts +29 -0
  67. package/dist-client/types.js.map +1 -1
  68. package/dist-client/viewparts/figure-thumb.d.ts +29 -0
  69. package/dist-client/viewparts/figure-thumb.js +242 -0
  70. package/dist-client/viewparts/figure-thumb.js.map +1 -0
  71. package/dist-server/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +6 -5
  73. package/test/i18n-prefix-guard.test.ts +171 -0
  74. package/test/korean-register-guard.test.ts +83 -0
  75. package/translations/en.json +351 -264
  76. package/translations/ja.json +276 -264
  77. package/translations/ko.json +351 -264
  78. package/translations/ms.json +276 -264
  79. package/translations/zh.json +276 -264
@@ -186,21 +186,21 @@ export class FigureAsk extends localize(i18next)(LitElement) {
186
186
  return html`
187
187
  <form @submit=${(e: Event) => this.submit(e)}>
188
188
  <md-icon lead>auto_awesome</md-icon>
189
- <span mode>${i18next.t(revising ? 'text.ask-to-revise' : 'text.ask-to-create')}</span>
189
+ <span mode>${i18next.t(revising ? 'figure.text.ask-to-revise' : 'figure.text.ask-to-create')}</span>
190
190
  <input
191
191
  ask
192
192
  ?disabled=${this.asking}
193
- placeholder=${i18next.t(revising ? 'text.ask-revise-example' : 'text.ask-create-example')}
193
+ placeholder=${i18next.t(revising ? 'figure.text.ask-revise-example' : 'figure.text.ask-create-example')}
194
194
  />
195
- <label picture ?has=${!!this.picture} title=${i18next.t('text.picture-is-for-proportions')}>
195
+ <label picture ?has=${!!this.picture} title=${i18next.t('figure.text.picture-is-for-proportions')}>
196
196
  <md-icon>image</md-icon>
197
197
  ${this.picture
198
198
  ? html`<span filename>${this.picture.name}</span>`
199
- : i18next.t('button.attach-a-picture')}
199
+ : i18next.t('figure.button.attach-a-picture')}
200
200
  <input type="file" accept="image/*" @change=${(e: Event) => this.pick(e)} />
201
201
  </label>
202
202
  <button go ?disabled=${this.asking}>
203
- ${this.asking ? i18next.t('text.asking') : i18next.t('button.ask')}
203
+ ${this.asking ? i18next.t('figure.text.asking') : i18next.t('figure.button.ask')}
204
204
  </button>
205
205
  </form>
206
206
 
@@ -302,7 +302,7 @@ export class FigureCanvas extends localize(i18next)(LitElement) {
302
302
  private standUp() {
303
303
  const board = this.board
304
304
  if (!board) {
305
- this.cannot = i18next.t('text.canvas-appears-once-a-part-exists')
305
+ this.cannot = i18next.t('figure.text.canvas-appears-once-a-part-exists')
306
306
  return
307
307
  }
308
308
 
@@ -0,0 +1,372 @@
1
+ import { css, html, LitElement } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+ import { live } from 'lit/directives/live.js'
4
+
5
+ import { i18next, localize } from '@operato/i18n'
6
+ import { ScrollbarStyles } from '@operato/styles'
7
+
8
+ import type { BoardModel, PartModel } from './figure-source.js'
9
+
10
+ /**
11
+ * 이 도형이 **무엇을 하는가.**
12
+ *
13
+ * ## 왜 탭 하나가 필요한가
14
+ *
15
+ * 지금까지 저작한 것은 **생김새**뿐이었다. 컨베이어를 아무리 잘 그려도 보드에 놓으면
16
+ * 상자 하나를 못 받는다 — 물건을 받고 담고 흘려보내는 것은 씬의 capability 가 하는 일이고,
17
+ * 손으로 쓴 컴포넌트는 클래스 선언 한 줄로 그것을 졌다. 저작한 도형에는 그 줄을 쓸 자리가
18
+ * 없어서, 여기서 고른 것이 그 줄이 된다.
19
+ *
20
+ * ## 사다리로 묻는다
21
+ *
22
+ * 능력의 이름(`Capacity` · `Holdable` · `Transferable` · `FlowNode`)을 그대로 늘어놓고
23
+ * 고르라고 하면, 저작자는 넷의 관계를 먼저 배워야 한다. 실제로 물어야 하는 것은 하나다 —
24
+ * **이 도형이 물건을 다루나, 다룬다면 지나가나.** 그래서 세 칸의 사다리로 묻고, 이름은
25
+ * 고른 뒤에 보여 준다.
26
+ *
27
+ * 윗 칸이 아랫 칸을 포함한다(`FlowNode` 가 나머지 셋을 스스로 흡수한다). 그래서 사다리이지
28
+ * 체크상자가 아니다.
29
+ */
30
+
31
+ /** 사다리 한 칸 — 화면이 묻는 말과, 그것이 되는 능력 이름. */
32
+ type Rung = 'none' | 'hold' | 'flow'
33
+
34
+ const RUNGS: Readonly<Record<Rung, readonly string[]>> = Object.freeze({
35
+ none: [],
36
+ hold: ['Capacity', 'Holdable'],
37
+ flow: ['FlowNode']
38
+ })
39
+
40
+ /** 선언된 이름 집합이 사다리 어느 칸인가. 어느 칸도 아니면 `null` — 지어내지 않는다. */
41
+ function rungOf(names: readonly string[] | undefined): Rung | null {
42
+ const has = new Set(names ?? [])
43
+ if (has.size === 0) return 'none'
44
+
45
+ for (const [rung, wanted] of Object.entries(RUNGS) as [Rung, readonly string[]][]) {
46
+ if (wanted.length === has.size && wanted.every(name => has.has(name))) return rung
47
+ }
48
+ return null
49
+ }
50
+
51
+ @customElement('figure-capabilities')
52
+ export class FigureCapabilities extends localize(i18next)(LitElement) {
53
+ static styles = [
54
+ ScrollbarStyles,
55
+ css`
56
+ :host {
57
+ display: flex;
58
+ flex-direction: column;
59
+ min-height: 0;
60
+ overflow-y: auto;
61
+ background-color: var(--md-sys-color-surface-container-lowest);
62
+ color: var(--md-sys-color-on-surface);
63
+ font: var(--label-font, inherit);
64
+ font-size: 0.76rem;
65
+ }
66
+
67
+ section {
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: var(--spacing-medium, 8px);
71
+ padding: var(--spacing-large, 12px);
72
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
73
+ }
74
+
75
+ /* 다른 탭들과 같은 눈금이다 — 탭마다 다른 제목 규격을 쓰면 따로 논다 */
76
+ h3 {
77
+ margin: 0 0 2px 0;
78
+ font-size: var(--ui-t-micro, 11px);
79
+ font-weight: 700;
80
+ letter-spacing: 0.04em;
81
+ text-transform: uppercase;
82
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
83
+ }
84
+
85
+ p[quiet] {
86
+ margin: 0;
87
+ font-size: 0.72rem;
88
+ line-height: 1.5;
89
+ color: var(--md-sys-color-on-surface-variant);
90
+ }
91
+
92
+ /* 사다리 — 고르는 것이 하나라 라디오다 */
93
+ label[rung] {
94
+ display: flex;
95
+ align-items: flex-start;
96
+ gap: var(--spacing-medium, 8px);
97
+ padding: 6px 8px;
98
+ border: 1px solid var(--md-sys-color-outline-variant);
99
+ border-radius: 4px;
100
+ cursor: pointer;
101
+ }
102
+ label[rung][on] {
103
+ border-color: var(--md-sys-color-primary);
104
+ background-color: var(--md-sys-color-surface-container);
105
+ }
106
+ label[rung]:hover:not([on]) {
107
+ background-color: var(--md-sys-color-surface-container-low);
108
+ }
109
+ label[rung] span {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 1px;
113
+ }
114
+ label[rung] strong {
115
+ font-weight: 600;
116
+ }
117
+ label[rung] em {
118
+ font-style: normal;
119
+ font-size: 0.7rem;
120
+ line-height: 1.4;
121
+ color: var(--md-sys-color-on-surface-variant);
122
+ }
123
+
124
+ input[type='radio'] {
125
+ flex: none;
126
+ margin: 2px 0 0 0;
127
+ accent-color: var(--md-sys-color-primary);
128
+ cursor: pointer;
129
+ }
130
+
131
+ /* 실제로 씬에 붙는 이름. 고른 뒤에 보여 준다 */
132
+ code[names] {
133
+ font: var(--mono-font, ui-monospace, monospace);
134
+ font-size: 0.7rem;
135
+ color: var(--md-sys-color-on-surface-variant);
136
+ }
137
+
138
+ /* 지정된 부품 한 줄 — 읽는 것이지 고치는 것이 아니다. 눌러 그 부품으로 간다 */
139
+ button[marked] {
140
+ display: flex;
141
+ align-items: baseline;
142
+ gap: var(--spacing-medium, 8px);
143
+ width: 100%;
144
+ padding: 5px 8px;
145
+ text-align: left;
146
+ font: inherit;
147
+ color: inherit;
148
+ background: none;
149
+ border: 1px solid var(--md-sys-color-outline-variant);
150
+ border-radius: 4px;
151
+ cursor: pointer;
152
+ }
153
+ button[marked]:hover {
154
+ background-color: var(--md-sys-color-surface-container);
155
+ }
156
+ button[marked] b {
157
+ flex: none;
158
+ font-weight: 600;
159
+ }
160
+ button[marked] span {
161
+ flex: none;
162
+ font-size: 0.72rem;
163
+ color: var(--md-sys-color-primary);
164
+ }
165
+ button[marked] em {
166
+ flex: 1;
167
+ min-width: 0;
168
+ overflow: hidden;
169
+ text-overflow: ellipsis;
170
+ white-space: nowrap;
171
+ text-align: right;
172
+ font-style: normal;
173
+ font-size: 0.7rem;
174
+ color: var(--md-sys-color-on-surface-variant);
175
+ }
176
+
177
+ /* 부품마다 한 줄. 자리를 고른 것만 아래로 펼친다 */
178
+ div[part] {
179
+ display: flex;
180
+ flex-direction: column;
181
+ gap: 4px;
182
+ padding: 6px 0;
183
+ border-top: 1px solid var(--md-sys-color-outline-variant);
184
+ }
185
+ div[part]:first-of-type {
186
+ border-top: none;
187
+ }
188
+ div[head] {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: var(--spacing-medium, 8px);
192
+ }
193
+ div[head] b {
194
+ flex: 1;
195
+ min-width: 0;
196
+ overflow: hidden;
197
+ text-overflow: ellipsis;
198
+ white-space: nowrap;
199
+ font-weight: 600;
200
+ }
201
+
202
+ select,
203
+ input[type='text'],
204
+ input[type='number'] {
205
+ padding: 4px 6px;
206
+ font: var(--input-field-font, inherit);
207
+ font-size: 0.74rem;
208
+ color: var(--md-sys-color-on-surface);
209
+ background-color: var(--md-sys-color-surface);
210
+ border: 1px solid var(--md-sys-color-outline-variant);
211
+ border-radius: 4px;
212
+ }
213
+ /* 이름이 길어질 수 있는 쪽(문)까지 안 잘리는 폭이다 */
214
+ div[head] select {
215
+ flex: none;
216
+ width: 8.5em;
217
+ }
218
+
219
+ div[detail] {
220
+ display: grid;
221
+ grid-template-columns: 4.5em 1fr;
222
+ align-items: center;
223
+ gap: 4px var(--spacing-medium, 8px);
224
+ padding-left: var(--spacing-medium, 8px);
225
+ }
226
+ div[detail] > span {
227
+ font-size: 0.72rem;
228
+ color: var(--md-sys-color-on-surface-variant);
229
+ }
230
+ div[detail] input {
231
+ width: 100%;
232
+ box-sizing: border-box;
233
+ }
234
+ div[detail] em {
235
+ grid-column: 2;
236
+ font-style: normal;
237
+ font-size: 0.7rem;
238
+ color: var(--md-sys-color-on-surface-variant);
239
+ }
240
+ `
241
+ ]
242
+
243
+ @property({ type: Object }) board?: BoardModel
244
+ @property({ type: Array }) parts: PartModel[] = []
245
+
246
+ render() {
247
+ const rung = rungOf(this.board?.capabilities)
248
+ const names = this.board?.capabilities ?? []
249
+
250
+ return html`
251
+ <section>
252
+ <h3>${i18next.t('figure.label.what-this-does')}</h3>
253
+
254
+ ${this.rung('none', rung)} ${this.rung('hold', rung)} ${this.rung('flow', rung)}
255
+ ${rung === null
256
+ ? html`<p quiet>${i18next.t('figure.text.capability-hand-written')}</p>`
257
+ : ''}
258
+ ${names.length > 0 ? html`<code names>${names.join(' · ')}</code>` : ''}
259
+ </section>
260
+
261
+ ${this.renderAnchors(rung)}
262
+ `
263
+ }
264
+
265
+ /**
266
+ * 자리와 문 — **여기서는 읽기만 한다.**
267
+ *
268
+ * 지정은 속성 탭에서 한다. 부품을 고치는 곳이 한 군데여야 하기 때문이다 — 목록을 여기 또
269
+ * 두면 같은 부품을 두 화면에서 고치게 되고, 저작자는 어느 쪽이 이기는지 매번 생각해야 한다.
270
+ *
271
+ * 그래도 요약은 남긴다. 「어느 부품이 자리인가」는 부품을 하나씩 눌러 보며 알아낼 일이
272
+ * 아니다 — 자리는 그려지지 않으므로 화면을 봐서는 영영 못 찾는다.
273
+ */
274
+ private renderAnchors(rung: Rung | null) {
275
+ if (rung === 'none') {
276
+ return html`
277
+ <section>
278
+ <h3>${i18next.t('figure.label.slots-and-ports')}</h3>
279
+ <p quiet>${i18next.t('figure.text.slots-need-capability')}</p>
280
+ </section>
281
+ `
282
+ }
283
+
284
+ const marked = this.parts.filter(part => part.capability)
285
+
286
+ return html`
287
+ <section>
288
+ <h3>${i18next.t('figure.label.slots-and-ports')}</h3>
289
+ <p quiet>${i18next.t('figure.text.anchors-are-not-drawn')}</p>
290
+
291
+ ${marked.length === 0
292
+ ? html`<p quiet>${i18next.t('figure.text.designate-in-properties')}</p>`
293
+ : marked.map(part => this.renderMarked(part))}
294
+ </section>
295
+ `
296
+ }
297
+
298
+ /** 지정된 부품 한 줄. 누르면 그 부품이 골라진다 — 고치는 자리는 속성 탭이다. */
299
+ private renderMarked(part: PartModel) {
300
+ const spec = part.capability!
301
+
302
+ return html`
303
+ <button marked @click=${() => this.pick(part)}>
304
+ <b>${part.name}</b>
305
+ <span>${spec.roles.map(role => i18next.t('figure.label.role-' + role)).join(' · ')}</span>
306
+ ${spec.accepts?.length ? html`<em>${spec.accepts.join(' · ')}</em>` : ''}
307
+ </button>
308
+ `
309
+ }
310
+
311
+ /** 이 부품을 고르게 한다. 부품 목록에서 누른 것과 같은 알림이다. */
312
+ private pick(part: PartModel) {
313
+ const index = this.parts.indexOf(part)
314
+ if (index < 0) return
315
+
316
+ this.dispatchEvent(new CustomEvent('select-part', { detail: { index }, bubbles: true, composed: true }))
317
+ }
318
+
319
+ /**
320
+ * 사다리 한 칸.
321
+ *
322
+ * `checked` 를 `live()` 로 묶는다. 그냥 두면 Lit 이 **마지막으로 그린 값**과 견주므로,
323
+ * 브라우저가 DOM 을 바꿨는데 모델은 안 바뀐 경우 되돌리지 않는다. 실제로 그랬다 — 고르는
324
+ * 도중 도형이 다시 실렸더니 점은 「흐른다」에 있고 강조 상자는 「그리기만 한다」에 있었다.
325
+ * 화면이 모델과 다른 말을 하는 상태다. `live()` 는 **지금 DOM 의 값**과 견준다.
326
+ */
327
+ private rung(which: Rung, chosen: Rung | null) {
328
+ return html`
329
+ <label rung ?on=${chosen === which}>
330
+ <input
331
+ type="radio"
332
+ name="rung"
333
+ .checked=${live(chosen === which)}
334
+ @change=${() => this.setRung(which)}
335
+ />
336
+ <span>
337
+ <strong>${i18next.t(`figure.label.rung-${which}`)}</strong>
338
+ <em>${i18next.t(`figure.text.rung-${which}-note`)}</em>
339
+ </span>
340
+ </label>
341
+ `
342
+ }
343
+
344
+ /**
345
+ * 사다리를 내리면 자리도 함께 거둔다.
346
+ *
347
+ * 능력을 「그리기만 한다」로 되돌려 놓고 부품에 자리 표시가 남아 있으면, 형식은 자리를
348
+ * 그대로 실어 보내는데 씬은 그것을 쓸 클래스가 없다. **선언은 있고 길이 없는** 상태가
349
+ * 되고, 그 도형은 멀쩡해 보이면서 아무것도 못 받는다.
350
+ */
351
+ private setRung(which: Rung) {
352
+ const board = this.board
353
+ if (!board) return
354
+
355
+ this.tellBoard({ ...board, capabilities: RUNGS[which].length > 0 ? [...RUNGS[which]] : undefined })
356
+
357
+ if (which === 'none') {
358
+ this.parts.forEach((part, index) => {
359
+ if (!part.capability) return
360
+ const { capability, ...rest } = part
361
+ this.dispatchEvent(
362
+ new CustomEvent('update-part', { detail: { index, part: rest }, bubbles: true, composed: true })
363
+ )
364
+ })
365
+ }
366
+ }
367
+
368
+ private tellBoard(board: BoardModel) {
369
+ this.board = board
370
+ this.dispatchEvent(new CustomEvent('update-board', { detail: { board }, bubbles: true, composed: true }))
371
+ }
372
+ }