@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,285 @@
1
+ import { css, html, LitElement } from 'lit'
2
+ import { customElement, property } from 'lit/decorators.js'
3
+
4
+ import { i18next, localize } from '@operato/i18n'
5
+ import { ScrollbarStyles } from '@operato/styles'
6
+
7
+ import { DEFAULT_VIEW, ENVIRONMENTS } from './figure-view.js'
8
+ import type { EnvironmentName, ViewSettings } from './figure-view.js'
9
+
10
+ /**
11
+ * 저작 화면의 **보기 설정**.
12
+ *
13
+ * ## 자산이 아니라 작업대다
14
+ *
15
+ * 여기서 고치는 것은 저장되지 않는다. 조명을 밝게 하고 바닥을 켠 것은 지금 보기 편한
16
+ * 상태일 뿐이다. 그래서 「저장 안 됨」을 화면에 적어 둔다 — 안 적으면 저작자가 조명을
17
+ * 맞춰 두고 그것이 자산에 딸려 간다고 여긴다.
18
+ *
19
+ * ## 값을 들고 있지 않는다
20
+ *
21
+ * 이 패널은 받은 것을 그리고 바뀐 것을 알린다. 정본은 모델러 페이지가 갖는다 — 속성
22
+ * 패널과 같은 방식이다. 여기서 들고 있으면 페이지가 다시 그릴 때 두 벌이 갈린다.
23
+ */
24
+ @customElement('figure-settings')
25
+ export class FigureSettings extends localize(i18next)(LitElement) {
26
+ static styles = [
27
+ ScrollbarStyles,
28
+ css`
29
+ :host {
30
+ display: flex;
31
+ flex-direction: column;
32
+ min-height: 0;
33
+ overflow-y: auto;
34
+ background-color: var(--md-sys-color-surface-container-lowest);
35
+ color: var(--md-sys-color-on-surface);
36
+ font: var(--label-font, inherit);
37
+ font-size: 0.76rem;
38
+ }
39
+
40
+ section {
41
+ display: flex;
42
+ flex-direction: column;
43
+ gap: var(--spacing-medium, 8px);
44
+ padding: var(--spacing-large, 12px);
45
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
46
+ }
47
+
48
+ /* 속성 패널의 묶음 제목과 같은 규격이다 — 두 탭이 다른 눈금을 쓰면 따로 논다 */
49
+ h3 {
50
+ margin: 0 0 2px 0;
51
+ font-size: var(--ui-t-micro, 11px);
52
+ font-weight: 700;
53
+ letter-spacing: 0.04em;
54
+ text-transform: uppercase;
55
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
56
+ }
57
+
58
+ /* 저장되지 않는다는 것을 한 번 말해 둔다 */
59
+ p[quiet] {
60
+ margin: 0;
61
+ font-size: 0.72rem;
62
+ line-height: 1.5;
63
+ color: var(--md-sys-color-on-surface-variant);
64
+ }
65
+
66
+ label[row] {
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: space-between;
70
+ gap: var(--spacing-medium, 8px);
71
+ cursor: pointer;
72
+ }
73
+ label[row] span {
74
+ display: flex;
75
+ flex-direction: column;
76
+ }
77
+ label[row] em {
78
+ font-style: normal;
79
+ font-size: 0.7rem;
80
+ color: var(--md-sys-color-on-surface-variant);
81
+ }
82
+
83
+ input[type='checkbox'] {
84
+ flex: none;
85
+ width: 16px;
86
+ height: 16px;
87
+ margin: 0;
88
+ accent-color: var(--md-sys-color-primary);
89
+ cursor: pointer;
90
+ }
91
+
92
+ label[field] {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 4px;
96
+ }
97
+ label[field] > span {
98
+ color: var(--md-sys-color-on-surface-variant);
99
+ }
100
+
101
+ select,
102
+ input[type='color'] {
103
+ width: 100%;
104
+ padding: 5px 6px;
105
+ font: var(--input-field-font, inherit);
106
+ font-size: 0.76rem;
107
+ color: var(--md-sys-color-on-surface);
108
+ background-color: var(--md-sys-color-surface);
109
+ border: 1px solid var(--md-sys-color-outline-variant);
110
+ border-radius: 4px;
111
+ }
112
+ input[type='color'] {
113
+ height: 30px;
114
+ padding: 2px;
115
+ cursor: pointer;
116
+ }
117
+
118
+ /* 값이 옆에 붙어야 끌면서 얼마인지 안다 */
119
+ div[slider] {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: var(--spacing-medium, 8px);
123
+ }
124
+ input[type='range'] {
125
+ flex: 1;
126
+ accent-color: var(--md-sys-color-primary);
127
+ cursor: pointer;
128
+ }
129
+ div[slider] output {
130
+ flex: none;
131
+ width: 2.4em;
132
+ text-align: right;
133
+ font: var(--mono-font, ui-monospace, monospace);
134
+ font-size: 0.72rem;
135
+ color: var(--md-sys-color-on-surface-variant);
136
+ }
137
+
138
+ button[reset] {
139
+ align-self: flex-start;
140
+ padding: 5px 12px;
141
+ font: var(--label-font, inherit);
142
+ font-size: 0.74rem;
143
+ color: var(--md-sys-color-on-surface-variant);
144
+ background: none;
145
+ border: 1px solid var(--md-sys-color-outline-variant);
146
+ border-radius: 4px;
147
+ cursor: pointer;
148
+ }
149
+ button[reset]:hover {
150
+ background-color: var(--md-sys-color-surface-container);
151
+ }
152
+ `
153
+ ]
154
+
155
+ @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
156
+
157
+ render() {
158
+ const view = this.view ?? DEFAULT_VIEW
159
+
160
+ return html`
161
+ <section>
162
+ <h3>${i18next.t('label.guides')}</h3>
163
+ <p quiet>${i18next.t('text.view-settings-are-not-saved')}</p>
164
+
165
+ ${this.toggle('grid', view.grid, i18next.t('label.coordinate-ground'), i18next.t('text.coordinate-ground-explained'))}
166
+ ${this.toggle('gridLabels', view.gridLabels, i18next.t('label.axis-labels'))}
167
+ ${this.toggle('floor', view.floor, i18next.t('label.floor'), i18next.t('text.floor-explained'))}
168
+ </section>
169
+
170
+ <section>
171
+ <h3>${i18next.t('label.environment')}</h3>
172
+
173
+ <label field>
174
+ <span>${i18next.t('label.preset')}</span>
175
+ <select
176
+ .value=${view.environment}
177
+ @change=${(e: Event) => this.put({ environment: (e.target as HTMLSelectElement).value as EnvironmentName })}
178
+ >
179
+ ${ENVIRONMENTS.map(
180
+ name => html`<option value=${name} ?selected=${view.environment === name}>
181
+ ${i18next.t(`label.environment-${name}`)}
182
+ </option>`
183
+ )}
184
+ </select>
185
+ </label>
186
+ <p quiet>${i18next.t('text.environment-explained')}</p>
187
+
188
+ <label field>
189
+ <span>${i18next.t('label.sky-color')}</span>
190
+ <input
191
+ type="color"
192
+ .value=${view.skyColor}
193
+ @change=${(e: Event) => this.put({ skyColor: (e.target as HTMLInputElement).value })}
194
+ />
195
+ </label>
196
+ </section>
197
+
198
+ <section>
199
+ <h3>${i18next.t('label.key-light')}</h3>
200
+
201
+ ${this.slider('dirLightIntensity', view.dirLightIntensity, i18next.t('label.intensity'), 0, 5, 0.1)}
202
+ ${this.toggle(
203
+ 'dirLightFollowCamera',
204
+ view.dirLightFollowCamera,
205
+ i18next.t('label.follow-camera'),
206
+ i18next.t('text.follow-camera-explained')
207
+ )}
208
+ ${this.toggle('dirShadowEnabled', view.dirShadowEnabled, i18next.t('label.cast-shadow'))}
209
+ ${view.dirLightIntensity === 0 && view.hemiIntensity === 0
210
+ ? html`<p quiet>${i18next.t('text.all-lights-off')}</p>`
211
+ : ''}
212
+ </section>
213
+
214
+ <section>
215
+ <h3>${i18next.t('label.hemisphere-light')}</h3>
216
+
217
+ ${this.slider('hemiIntensity', view.hemiIntensity, i18next.t('label.intensity'), 0, 8, 0.1)}
218
+ <p quiet>${i18next.t('text.hemisphere-light-explained')}</p>
219
+ </section>
220
+
221
+ <section>
222
+ <button reset @click=${() => this.reset()}>${i18next.t('button.reset-view')}</button>
223
+ </section>
224
+ `
225
+ }
226
+
227
+ private toggle(key: keyof ViewSettings, on: boolean, label: string, hint?: string) {
228
+ return html`
229
+ <label row>
230
+ <span>
231
+ ${label}${hint ? html`<em>${hint}</em>` : ''}
232
+ </span>
233
+ <input
234
+ type="checkbox"
235
+ .checked=${on}
236
+ @change=${(e: Event) => this.put({ [key]: (e.target as HTMLInputElement).checked } as Partial<ViewSettings>)}
237
+ />
238
+ </label>
239
+ `
240
+ }
241
+
242
+ private slider(key: keyof ViewSettings, value: number, label: string, min: number, max: number, step: number) {
243
+ return html`
244
+ <label field>
245
+ <span>${label}</span>
246
+ <div slider>
247
+ <input
248
+ type="range"
249
+ min=${min}
250
+ max=${max}
251
+ step=${step}
252
+ .value=${String(value)}
253
+ @input=${(e: Event) =>
254
+ this.put({ [key]: Number((e.target as HTMLInputElement).value) } as Partial<ViewSettings>)}
255
+ />
256
+ <output>${value.toFixed(1)}</output>
257
+ </div>
258
+ </label>
259
+ `
260
+ }
261
+
262
+ /** 처음 값으로 되돌린다. 만지다 길을 잃었을 때 돌아올 자리가 있어야 한다. */
263
+ private reset() {
264
+ this.tell({ ...DEFAULT_VIEW })
265
+ }
266
+
267
+ private put(patch: Partial<ViewSettings>) {
268
+ this.tell({ ...(this.view ?? DEFAULT_VIEW), ...patch })
269
+ }
270
+
271
+ /**
272
+ * 바뀐 것을 알린다.
273
+ *
274
+ * **알리기 전에 내 것도 바꿔 둔다.** Lit 은 속성을 다음 렌더에 넣으므로, 같은 프레임에
275
+ * 두 번 고치면 두 번째가 `this.view` 의 옛 값을 읽어 첫 번째를 덮는다. 실제로 그랬다 —
276
+ * 축 가이드를 끄고 바닥판을 켰더니 축 가이드가 도로 켜져 있었고, 화면의 체크 상자와
277
+ * 씬의 상태가 서로 다른 말을 했다.
278
+ *
279
+ * 정본은 여전히 모델러 페이지가 갖는다. 여기 값은 다음 렌더에 같은 것으로 덮인다.
280
+ */
281
+ private tell(view: ViewSettings) {
282
+ this.view = view
283
+ this.dispatchEvent(new CustomEvent('update-view', { detail: { view }, bubbles: true, composed: true }))
284
+ }
285
+ }
@@ -0,0 +1,162 @@
1
+ import './figure-inspector.js'
2
+ import './figure-report.js'
3
+ import './figure-settings.js'
4
+
5
+ import { css, html, LitElement } from 'lit'
6
+ import { customElement, property, state } from 'lit/decorators.js'
7
+
8
+ import { i18next, localize } from '@operato/i18n'
9
+ import type { FigureSource } from '@hatiolab/figure-model'
10
+
11
+ import type { BoardModel, PartModel } from './figure-source.js'
12
+ import { DEFAULT_VIEW } from './figure-view.js'
13
+ import type { ViewSettings } from './figure-view.js'
14
+
15
+ type Tab = 'properties' | 'verdict' | 'view'
16
+
17
+ /**
18
+ * 저작면 오른쪽 칸 — 속성과 판정을 탭으로 겹쳐 둔다.
19
+ *
20
+ * ## 왜 위아래로 나누지 않나
21
+ *
22
+ * 둘 다 세로로 긴 화면이다. 위아래로 나누면 양쪽 다 잘려서, 속성은 재질까지 못
23
+ * 내려가고 판정은 무게까지 못 내려간다. 탭으로 겹치면 각자 칸 전체를 쓴다.
24
+ *
25
+ * ## 그래도 개수는 늘 보인다
26
+ *
27
+ * 성숙도의 축 하나 —「몇 개까지 편한가」— 는 부품을 고칠 때마다 움직이고, 그것이
28
+ * 보여야 저작자가 고칠 수 있다. 탭 뒤로 숨으면 저장하고 나서야 알게 된다. 그래서 그
29
+ * 숫자만 **탭 이름표에 남긴다** — 성숙도를 덮어 두고 속성을 만져도 따라 움직이는
30
+ * 것이 보인다.
31
+ *
32
+ * 판정은 화면에서 감춰도 **DOM 에는 남는다.** 떼었다 붙이면 다시 세느라 값이 잠깐
33
+ * 비고, 펼쳐 둔 추이 그림 같은 상태도 날아간다.
34
+ */
35
+ @customElement('figure-side')
36
+ export class FigureSide extends localize(i18next)(LitElement) {
37
+ static styles = css`
38
+ :host {
39
+ display: flex;
40
+ flex-direction: column;
41
+ min-height: 0;
42
+ overflow: hidden;
43
+ background-color: var(--md-sys-color-surface-container-lowest);
44
+ border-left: 1px solid var(--md-sys-color-outline-variant);
45
+ color: var(--md-sys-color-on-surface);
46
+ font: var(--label-font, inherit);
47
+ /*
48
+ 이 패널의 본문 크기를 여기서 한 번 정한다.
49
+
50
+ 전에는 규칙마다 따로 적었다. 그러면 빠뜨린 규칙이 본문 기본 크기(1rem)를
51
+ 물려받아 그 줄만 제목처럼 커진다. 오류가 아니라서 화면을 띄워 봐야 안다.
52
+ 이 파일에서만 두 번 났다.
53
+
54
+ form 요소(input · select · button)는 글꼴을 물려받지 않으므로 각자 적어야 한다.
55
+ 그것 말고는 여기 값을 따른다.
56
+ */
57
+ font-size: 0.76rem;
58
+ }
59
+
60
+ div[tabs] {
61
+ display: flex;
62
+ flex: none;
63
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
64
+ }
65
+ button[tab] {
66
+ flex: 1;
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ gap: 6px;
71
+ padding: 8px 6px;
72
+ font: var(--label-font, inherit);
73
+ font-size: 0.76rem;
74
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
75
+ background: none;
76
+ border: none;
77
+ /* 고른 탭을 밑줄로 말한다 — 칸을 색으로 채우면 아래 내용과 경계가 흐려진다 */
78
+ border-bottom: 2px solid transparent;
79
+ cursor: pointer;
80
+ }
81
+ button[tab]:hover:not([on]) {
82
+ background-color: var(--md-sys-color-surface-container);
83
+ }
84
+ button[tab][on] {
85
+ font-weight: 700;
86
+ color: var(--md-sys-color-primary);
87
+ border-bottom-color: var(--md-sys-color-primary);
88
+ }
89
+
90
+ /* 판정을 덮어 두어도 개수는 여기 남는다 */
91
+ span[badge] {
92
+ padding: 1px 7px;
93
+ font-size: 0.72rem;
94
+ font-weight: 700;
95
+ color: var(--md-sys-color-on-primary-container);
96
+ background-color: var(--md-sys-color-primary-container);
97
+ border-radius: 999em;
98
+ }
99
+
100
+ figure-inspector,
101
+ figure-report,
102
+ figure-settings {
103
+ flex: 1;
104
+ min-height: 0;
105
+ border-left: none;
106
+ }
107
+ /* 감추되 떼지는 않는다 */
108
+ [off] {
109
+ display: none;
110
+ }
111
+ `
112
+
113
+ /*
114
+ 두 형태가 이 패널에서 갈린다.
115
+
116
+ 속성은 **수정하는** 곳이라 편집 중인 씬 모델을 받는다. 판정은 **저장될 것을 측정하는**
117
+ 곳이라 변환된 FigureSource 를 받는다. 변환은 모델러 페이지가 한 번만 해서 내려보낸다.
118
+ */
119
+ @property({ type: Object }) board?: BoardModel
120
+ @property({ type: Array }) parts: PartModel[] = []
121
+ @property({ type: Object }) source?: FigureSource
122
+ @property({ type: Number }) selected = -1
123
+ /** 저작 화면의 보기 설정. 자산이 아니라 작업대다 — 저장되지 않는다. */
124
+ @property({ type: Object }) view: ViewSettings = DEFAULT_VIEW
125
+
126
+ @state() private tab: Tab = 'properties'
127
+ /** 판정이 알려 준 개수. 탭 이름표에 붙는다. */
128
+ @state() private instances?: number
129
+
130
+ render() {
131
+ return html`
132
+ <div tabs>
133
+ ${this.renderTab('properties', i18next.t('label.properties'))}
134
+ ${this.renderTab('verdict', i18next.t('label.maturity'), this.instances)}
135
+ ${this.renderTab('view', i18next.t('label.view'))}
136
+ </div>
137
+
138
+ <figure-inspector
139
+ ?off=${this.tab !== 'properties'}
140
+ .board=${this.board}
141
+ .parts=${this.parts}
142
+ .selected=${this.selected}
143
+ ></figure-inspector>
144
+
145
+ <figure-report
146
+ ?off=${this.tab !== 'verdict'}
147
+ .source=${this.source}
148
+ @verdict-changed=${(e: CustomEvent) => (this.instances = e.detail.instances)}
149
+ ></figure-report>
150
+
151
+ <figure-settings ?off=${this.tab !== 'view'} .view=${this.view}></figure-settings>
152
+ `
153
+ }
154
+
155
+ private renderTab(tab: Tab, label: string, badge?: number) {
156
+ return html`
157
+ <button tab ?on=${this.tab === tab} @click=${() => (this.tab = tab)}>
158
+ ${label}${badge == null ? '' : html`<span badge>${badge.toLocaleString()}</span>`}
159
+ </button>
160
+ `
161
+ }
162
+ }