@things-factory/figure-ui 10.1.38 → 10.1.39
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/client/graphql/index.ts +53 -0
- package/client/modeller/figure-history-panel.ts +11 -85
- package/client/modeller/figure-v3-verdict.ts +247 -0
- package/client/modeller/gate-findings.ts +105 -0
- package/client/pages/figure-v3-modeller-page.ts +193 -15
- package/dist-client/graphql/index.d.ts +8 -0
- package/dist-client/graphql/index.js +51 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.d.ts +6 -0
- package/dist-client/modeller/figure-history-panel.js +9 -85
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +50 -0
- package/dist-client/modeller/figure-v3-verdict.js +268 -0
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -0
- package/dist-client/modeller/gate-findings.d.ts +12 -0
- package/dist-client/modeller/gate-findings.js +102 -0
- package/dist-client/modeller/gate-findings.js.map +1 -0
- package/dist-client/pages/figure-v3-modeller-page.d.ts +26 -0
- package/dist-client/pages/figure-v3-modeller-page.js +204 -14
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/v3-verdict.test.ts +82 -0
- package/translations/en.json +6 -0
- package/translations/ja.json +6 -0
- package/translations/ko.json +6 -0
- package/translations/ms.json +6 -0
- package/translations/zh.json +6 -0
package/client/graphql/index.ts
CHANGED
|
@@ -335,6 +335,59 @@ export async function inspectFigure(id: string): Promise<FigureInspection> {
|
|
|
335
335
|
return unwrap<FigureInspection>(response, 'inspectFigure')
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.
|
|
340
|
+
*
|
|
341
|
+
* 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는
|
|
342
|
+
* 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —
|
|
343
|
+
* 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행판을 읽는다.
|
|
344
|
+
*/
|
|
345
|
+
export async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {
|
|
346
|
+
const response = await client.query({
|
|
347
|
+
query: gql`
|
|
348
|
+
query InspectFigureDraft($source: String!, $id: String) {
|
|
349
|
+
inspectFigureDraft(source: $source, id: $id) {
|
|
350
|
+
blocked
|
|
351
|
+
findings {
|
|
352
|
+
code
|
|
353
|
+
message
|
|
354
|
+
why
|
|
355
|
+
how
|
|
356
|
+
at
|
|
357
|
+
fixes {
|
|
358
|
+
findingWay
|
|
359
|
+
part
|
|
360
|
+
axis
|
|
361
|
+
from
|
|
362
|
+
to
|
|
363
|
+
resolved
|
|
364
|
+
safe
|
|
365
|
+
}
|
|
366
|
+
fixPlan {
|
|
367
|
+
resolved
|
|
368
|
+
safe
|
|
369
|
+
fixes {
|
|
370
|
+
findingWay
|
|
371
|
+
part
|
|
372
|
+
axis
|
|
373
|
+
from
|
|
374
|
+
to
|
|
375
|
+
resolved
|
|
376
|
+
safe
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
blocking
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
`,
|
|
384
|
+
variables: { source, id },
|
|
385
|
+
fetchPolicy: 'network-only'
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
return unwrap<FigureInspection>(response, 'inspectFigureDraft')
|
|
389
|
+
}
|
|
390
|
+
|
|
338
391
|
/** 발행된 판들 — 최근 것부터 열 개. */
|
|
339
392
|
/**
|
|
340
393
|
* 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js'
|
|
2
2
|
|
|
3
|
+
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
3
4
|
import { css, html, LitElement, nothing } from 'lit'
|
|
4
5
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
6
|
|
|
@@ -31,6 +32,7 @@ import type { FigureGateFinding, FigureGateFix, FigureGateFixPlan, FigureInspect
|
|
|
31
32
|
@customElement('figure-history-panel')
|
|
32
33
|
export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
33
34
|
static styles = [
|
|
35
|
+
gateFindingStyles,
|
|
34
36
|
ScrollbarStyles,
|
|
35
37
|
css`
|
|
36
38
|
:host {
|
|
@@ -191,77 +193,6 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
191
193
|
게이트 사유. 막는 것은 색으로도 말하고, 알리는 것은 조용히 둔다 — 둘을 같은 톤으로 두면
|
|
192
194
|
「이 중 어느 것이 발행을 막았나」를 사람이 다시 세어야 한다.
|
|
193
195
|
*/
|
|
194
|
-
ul[gate] {
|
|
195
|
-
margin: var(--spacing-small, 6px) 0 0;
|
|
196
|
-
padding: 0;
|
|
197
|
-
list-style: none;
|
|
198
|
-
display: flex;
|
|
199
|
-
flex-direction: column;
|
|
200
|
-
gap: 3px;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
li[head] {
|
|
204
|
-
font: var(--label-font, inherit);
|
|
205
|
-
font-size: 0.72rem;
|
|
206
|
-
color: var(--md-sys-color-error);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
li[head][quiet] {
|
|
210
|
-
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
li[finding] {
|
|
214
|
-
display: flex;
|
|
215
|
-
flex-direction: column;
|
|
216
|
-
align-items: flex-start;
|
|
217
|
-
gap: 3px;
|
|
218
|
-
padding: 3px 6px;
|
|
219
|
-
font-size: 0.72rem;
|
|
220
|
-
line-height: 1.5;
|
|
221
|
-
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
222
|
-
background-color: var(--md-sys-color-surface-container);
|
|
223
|
-
border-radius: 4px;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
li[finding][blocking] {
|
|
227
|
-
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
228
|
-
background-color: var(--md-sys-color-error-container);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
li[finding] code[where] {
|
|
232
|
-
font-family: var(--mono-font, monospace);
|
|
233
|
-
font-size: 0.68rem;
|
|
234
|
-
opacity: 0.85;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
li[finding] details[affected] {
|
|
238
|
-
width: 100%;
|
|
239
|
-
color: inherit;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
li[finding] details[affected] summary {
|
|
243
|
-
cursor: pointer;
|
|
244
|
-
font-weight: 600;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */
|
|
248
|
-
li[finding] span[why-line] {
|
|
249
|
-
opacity: 0.8;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/* 무엇을 바꾸면 되나 — 이 줄이 실제로 사람이 할 일이다. 연장 표를 달아 눈에 걸리게 한다 */
|
|
253
|
-
li[finding] span[how-line] {
|
|
254
|
-
display: flex;
|
|
255
|
-
align-items: flex-start;
|
|
256
|
-
gap: 4px;
|
|
257
|
-
padding-top: 2px;
|
|
258
|
-
font-weight: 600;
|
|
259
|
-
}
|
|
260
|
-
li[finding] span[how-line] md-icon {
|
|
261
|
-
--md-icon-size: 13px;
|
|
262
|
-
flex: none;
|
|
263
|
-
margin-top: 1px;
|
|
264
|
-
}
|
|
265
196
|
|
|
266
197
|
/* 설명으로 끝내지 않는다. 이 단추는 원본을 바꾸지 않고 모델러에 후보만 연다. */
|
|
267
198
|
li[finding] button[preview-fix] {
|
|
@@ -332,6 +263,12 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
332
263
|
* 화면이 없는 단추를 그리면 눌러도 아무 일이 없다.
|
|
333
264
|
*/
|
|
334
265
|
@property({ type: String }) format: 'v2' | 'v3' = 'v2'
|
|
266
|
+
/**
|
|
267
|
+
* 행의 타입. 판정 하나(`type-is-placeholder`)가 이것을 본다 — V3 자산은 타입을 원본에 적지 않아서, 타입을 바꾸면
|
|
268
|
+
* 원본도 dirty 도 그대로다. 그래서 이 값이 바뀌면 판정을 다시 묻는다. 안 그러면 타입을 정한 뒤에도 「임시 타입」
|
|
269
|
+
* 판정이 남아 발행 단추가 잠긴 채다.
|
|
270
|
+
*/
|
|
271
|
+
@property({ type: String }) figureType?: string
|
|
335
272
|
/** 직전 발행판의 기준 상자. 외형 변화 안내에서 X·Z 복원 선택지에만 쓴다. */
|
|
336
273
|
@property({ attribute: false }) releasedBase?: { x: number; y: number; z: number }
|
|
337
274
|
|
|
@@ -354,7 +291,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
354
291
|
저장이 끝난 순간(dirty 가 풀림) 정본이 바뀌었으니 판정을 다시 묻는다. 저작 중(dirty)에는
|
|
355
292
|
묻지 않는다 — 서버에 있는 것은 저장 전 정본이라, 그 답은 화면에 있는 것과 다른 것을 말한다.
|
|
356
293
|
*/
|
|
357
|
-
if (changes.has('figureId') || changes.has('dirty')) {
|
|
294
|
+
if (changes.has('figureId') || changes.has('dirty') || changes.has('figureType')) {
|
|
358
295
|
void this.inspect()
|
|
359
296
|
}
|
|
360
297
|
}
|
|
@@ -474,17 +411,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
474
411
|
* 준다 — 화면이 문장을 지으면 서버가 거절할 때와 다른 말을 하게 된다.
|
|
475
412
|
*/
|
|
476
413
|
private renderFinding(finding: FigureGateFinding, blocking: boolean) {
|
|
477
|
-
return html`
|
|
478
|
-
<li finding ?blocking=${blocking}>
|
|
479
|
-
<span msg>${finding.message}</span>
|
|
480
|
-
${finding.at ? html`
|
|
481
|
-
<details affected>
|
|
482
|
-
<summary>${i18next.t('figure.text.show-affected-parts')}</summary>
|
|
483
|
-
<code where>${finding.at}</code>
|
|
484
|
-
</details>
|
|
485
|
-
` : nothing}
|
|
486
|
-
${finding.why ? html`<span why-line>${finding.why}</span>` : nothing}
|
|
487
|
-
${finding.how ? html`<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}
|
|
414
|
+
return renderGateFinding(finding, blocking, html`
|
|
488
415
|
${this.offersFit(finding.code)
|
|
489
416
|
? html`<button fit-base @click=${() => this.fitBase()}>
|
|
490
417
|
${i18next.t('figure.button.fit-base-to-parts')}
|
|
@@ -522,8 +449,7 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
522
449
|
${i18next.t('figure.button.apply-recommended-fix')}
|
|
523
450
|
</button>
|
|
524
451
|
` : nothing}
|
|
525
|
-
|
|
526
|
-
`
|
|
452
|
+
`)
|
|
527
453
|
}
|
|
528
454
|
|
|
529
455
|
/**
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js'
|
|
2
|
+
|
|
3
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
|
+
|
|
6
|
+
import { i18next, localize } from '@operato/i18n'
|
|
7
|
+
import { parseV3Asset, v3CostOf } from '@hatiolab/figure-model'
|
|
8
|
+
|
|
9
|
+
import { inspectFigureDraft } from '../graphql/index.js'
|
|
10
|
+
import type { FigureInspection } from '../types.js'
|
|
11
|
+
import { gateFindingStyles, renderGateFinding } from './gate-findings.js'
|
|
12
|
+
import { capacityFrom, MEASURED } from './scene-budget.js'
|
|
13
|
+
import type { FigureCapacity } from './scene-budget.js'
|
|
14
|
+
|
|
15
|
+
/** How long an edit settles before the server is asked again. One question per pause, not one per keystroke. */
|
|
16
|
+
export const VERDICT_SETTLE_MS = 400
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 판정 — the V3 modeller's verdict while editing (parity row ①).
|
|
20
|
+
*
|
|
21
|
+
* The V2 modeller shows, as the author edits, how many of the figure fit on one screen and what would stop it
|
|
22
|
+
* being published. The V3 page showed neither: the author learned what blocked a release only after saving, in the
|
|
23
|
+
* release panel. This tab answers both from the source being edited.
|
|
24
|
+
*
|
|
25
|
+
* The count is computed here, from `v3CostOf` and the same `capacityFrom` the catalogue cards use, so the tab and
|
|
26
|
+
* the card cannot disagree. The findings are not: they come from the server's release door (`inspectFigureDraft`),
|
|
27
|
+
* the same door and the same sentences as the release panel. A second judgement on the screen would say something
|
|
28
|
+
* different the first time either changed.
|
|
29
|
+
*
|
|
30
|
+
* No grade. V2's grade measures V2 reuse; nothing measures it for V3, so the tab shows no letter rather than one
|
|
31
|
+
* nothing computed.
|
|
32
|
+
*/
|
|
33
|
+
@customElement('figure-v3-verdict')
|
|
34
|
+
export class FigureV3Verdict extends localize(i18next)(LitElement) {
|
|
35
|
+
static styles = [
|
|
36
|
+
gateFindingStyles,
|
|
37
|
+
css`
|
|
38
|
+
:host {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: 14px;
|
|
42
|
+
padding: 12px;
|
|
43
|
+
overflow: auto;
|
|
44
|
+
font-size: 0.76rem;
|
|
45
|
+
color: var(--md-sys-color-on-surface);
|
|
46
|
+
}
|
|
47
|
+
section > h3 {
|
|
48
|
+
margin: 0 0 6px 0;
|
|
49
|
+
font-size: var(--ui-t-micro, 11px);
|
|
50
|
+
font-weight: 700;
|
|
51
|
+
letter-spacing: 0.04em;
|
|
52
|
+
text-transform: uppercase;
|
|
53
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
54
|
+
}
|
|
55
|
+
p {
|
|
56
|
+
margin: 4px 0 0;
|
|
57
|
+
}
|
|
58
|
+
p[lead],
|
|
59
|
+
p[basis],
|
|
60
|
+
p[quiet] {
|
|
61
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
62
|
+
}
|
|
63
|
+
div[count] {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: baseline;
|
|
66
|
+
gap: 6px;
|
|
67
|
+
}
|
|
68
|
+
span[capacity] {
|
|
69
|
+
font-size: 2.1rem;
|
|
70
|
+
font-weight: 800;
|
|
71
|
+
line-height: 1;
|
|
72
|
+
letter-spacing: -0.01em;
|
|
73
|
+
color: var(--md-sys-color-primary);
|
|
74
|
+
}
|
|
75
|
+
span[unit] {
|
|
76
|
+
font-size: 0.9rem;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
color: var(--md-sys-color-primary);
|
|
79
|
+
}
|
|
80
|
+
p[clear] {
|
|
81
|
+
color: var(--md-sys-color-primary);
|
|
82
|
+
font-weight: 600;
|
|
83
|
+
}
|
|
84
|
+
p[failed] {
|
|
85
|
+
color: var(--md-sys-color-error);
|
|
86
|
+
}
|
|
87
|
+
button[fit] {
|
|
88
|
+
align-self: flex-start;
|
|
89
|
+
margin-top: 4px;
|
|
90
|
+
padding: 4px 10px;
|
|
91
|
+
font: inherit;
|
|
92
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
93
|
+
background: var(--md-sys-color-secondary-container);
|
|
94
|
+
border: none;
|
|
95
|
+
border-radius: 999em;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
/** The source being edited, serialised. Every change of it is a new question. */
|
|
102
|
+
@property({ type: String }) source?: string
|
|
103
|
+
/** The saved row, if any — the server reads its type and last release from it. */
|
|
104
|
+
@property({ type: String }) figureId?: string
|
|
105
|
+
/** Changing the type changes the verdict (a temporary type is refused), so it is a reason to ask again. */
|
|
106
|
+
@property({ type: String }) figureType?: string
|
|
107
|
+
|
|
108
|
+
@state() private capacity?: FigureCapacity
|
|
109
|
+
/** Bodies in the asset, drawn or hidden. With none there is nothing to fit the space to, so the button is not offered. */
|
|
110
|
+
@state() private bodies = 0
|
|
111
|
+
@state() private inspection?: FigureInspection
|
|
112
|
+
@state() private failure = ''
|
|
113
|
+
@state() private asking = false
|
|
114
|
+
|
|
115
|
+
private timer?: ReturnType<typeof setTimeout>
|
|
116
|
+
/** Answers can come back out of order; only the newest question's answer is shown. */
|
|
117
|
+
private asked = 0
|
|
118
|
+
|
|
119
|
+
disconnectedCallback() {
|
|
120
|
+
clearTimeout(this.timer)
|
|
121
|
+
super.disconnectedCallback()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
updated(changes: Map<string, unknown>) {
|
|
125
|
+
if (changes.has('source')) this.measure()
|
|
126
|
+
if (changes.has('source') || changes.has('figureId') || changes.has('figureType')) this.schedule()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private measure() {
|
|
130
|
+
let capacity: FigureCapacity | undefined
|
|
131
|
+
let bodies = 0
|
|
132
|
+
try {
|
|
133
|
+
const cost = this.source ? v3CostOf(parseV3Asset(this.source)) : undefined
|
|
134
|
+
capacity = cost ? capacityFrom(cost.groups, cost.triangles) : undefined
|
|
135
|
+
bodies = cost ? cost.groups + cost.hidden : 0
|
|
136
|
+
} catch {
|
|
137
|
+
/* A source the format refuses has no count; the server's finding below says why. */
|
|
138
|
+
capacity = undefined
|
|
139
|
+
}
|
|
140
|
+
this.capacity = capacity
|
|
141
|
+
this.bodies = bodies
|
|
142
|
+
this.dispatchEvent(new CustomEvent('capacity-changed', { detail: { instances: capacity?.instances } }))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private schedule() {
|
|
146
|
+
clearTimeout(this.timer)
|
|
147
|
+
if (!this.source) {
|
|
148
|
+
this.inspection = undefined
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
this.timer = setTimeout(() => this.ask(), VERDICT_SETTLE_MS)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async ask() {
|
|
155
|
+
const question = ++this.asked
|
|
156
|
+
const source = this.source!
|
|
157
|
+
this.asking = true
|
|
158
|
+
try {
|
|
159
|
+
const inspection = await inspectFigureDraft(source, this.figureId)
|
|
160
|
+
if (question !== this.asked) return
|
|
161
|
+
this.inspection = inspection
|
|
162
|
+
this.failure = ''
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (question !== this.asked) return
|
|
165
|
+
/* The server's reason as it gave it — the screen does not make one up. The last answer is dropped with it. */
|
|
166
|
+
this.inspection = undefined
|
|
167
|
+
this.failure = error instanceof Error ? error.message : String(error)
|
|
168
|
+
} finally {
|
|
169
|
+
if (question === this.asked) this.asking = false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
render() {
|
|
174
|
+
return html`${this.renderCount()} ${this.renderGate()}`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private renderCount() {
|
|
178
|
+
const capacity = this.capacity
|
|
179
|
+
return html`
|
|
180
|
+
<section>
|
|
181
|
+
<h3>${i18next.t('figure.label.maturity')}</h3>
|
|
182
|
+
<p lead>${i18next.t('figure.text.how-many-fit')}</p>
|
|
183
|
+
${capacity
|
|
184
|
+
? html`
|
|
185
|
+
<div count>
|
|
186
|
+
<span capacity>${capacity.instances.toLocaleString()}</span>
|
|
187
|
+
<span unit>${i18next.t('figure.text.instances-unit')}</span>
|
|
188
|
+
</div>
|
|
189
|
+
<p basis>
|
|
190
|
+
${i18next.t('figure.text.bound-by-' + capacity.bound, {
|
|
191
|
+
per: capacity.per.toLocaleString(),
|
|
192
|
+
budget: capacity.budget.toLocaleString()
|
|
193
|
+
})}
|
|
194
|
+
<em>
|
|
195
|
+
${capacity.bound === 'groups'
|
|
196
|
+
? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })
|
|
197
|
+
: i18next.t('figure.text.budget-not-measured-yet')}
|
|
198
|
+
</em>
|
|
199
|
+
</p>
|
|
200
|
+
`
|
|
201
|
+
: html`<p quiet>${i18next.t('figure.text.maturity-blocked-no-parts')}</p>`}
|
|
202
|
+
</section>
|
|
203
|
+
`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private renderGate() {
|
|
207
|
+
const findings = this.inspection?.findings ?? []
|
|
208
|
+
const blocking = findings.filter(finding => finding.blocking)
|
|
209
|
+
const advisory = findings.filter(finding => !finding.blocking)
|
|
210
|
+
return html`
|
|
211
|
+
<section>
|
|
212
|
+
<h3>${i18next.t('figure.label.release-verdict')}</h3>
|
|
213
|
+
${this.failure
|
|
214
|
+
? html`<p failed role="alert">${i18next.t('figure.text.verdict-not-received', { reason: this.failure })}</p>`
|
|
215
|
+
: !this.inspection
|
|
216
|
+
? html`<p quiet>${i18next.t(this.asking ? 'figure.text.verdict-asking' : 'figure.text.verdict-waiting')}</p>`
|
|
217
|
+
: blocking.length
|
|
218
|
+
? html`<p>${i18next.t('figure.text.cannot-release-now')}</p>`
|
|
219
|
+
: html`<p clear>${i18next.t('figure.text.nothing-blocks-release')}</p>`}
|
|
220
|
+
${blocking.length
|
|
221
|
+
? html`<ul gate>
|
|
222
|
+
<li head>${i18next.t('figure.text.must-fix-to-release')}</li>
|
|
223
|
+
${blocking.map(finding => renderGateFinding(finding, true, this.actionsFor(finding.code)))}
|
|
224
|
+
</ul>`
|
|
225
|
+
: nothing}
|
|
226
|
+
${advisory.length
|
|
227
|
+
? html`<ul gate>
|
|
228
|
+
<li head quiet>${i18next.t('figure.text.does-not-block')}</li>
|
|
229
|
+
${advisory.map(finding => renderGateFinding(finding, false))}
|
|
230
|
+
</ul>`
|
|
231
|
+
: nothing}
|
|
232
|
+
</section>
|
|
233
|
+
`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The same way out the release panel offers for the same two findings — declare the space from the parts. */
|
|
237
|
+
private actionsFor(code: string) {
|
|
238
|
+
if (code !== 'part-outside-occupancy' && code !== 'occupancy-undeclared') return nothing
|
|
239
|
+
/* A new figure with no parts is refused as undeclared too, but fitting needs parts to measure (seen 2026-09-24). */
|
|
240
|
+
if (!this.bodies) return nothing
|
|
241
|
+
return html`<button fit @click=${this.fit}>${i18next.t('figure.button.fit-base-to-parts')}</button>`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private fit = () => {
|
|
245
|
+
this.dispatchEvent(new CustomEvent('fit-base-to-parts', { bubbles: true, composed: true }))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { css, html, nothing, type TemplateResult } from 'lit'
|
|
2
|
+
import { i18next } from '@operato/i18n'
|
|
3
|
+
|
|
4
|
+
import type { FigureGateFinding } from '../types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One release-gate finding as a line on screen — the measured fact, where, why it matters, and what to change.
|
|
8
|
+
*
|
|
9
|
+
* Two places draw these: the release panel (`figure-history-panel`) and the V3 modeller's verdict while editing
|
|
10
|
+
* (`figure-v3-verdict`). They show the same findings from the same server door, so they draw them one way; a copy
|
|
11
|
+
* in each would drift the first time one of them is touched. The caller passes its own action buttons, because
|
|
12
|
+
* what can be done about a finding depends on the screen it is shown on.
|
|
13
|
+
*/
|
|
14
|
+
export function renderGateFinding(finding: FigureGateFinding, blocking: boolean, actions: unknown = nothing): TemplateResult {
|
|
15
|
+
return html`
|
|
16
|
+
<li finding ?blocking=${blocking}>
|
|
17
|
+
<span msg>${finding.message}</span>
|
|
18
|
+
${finding.at
|
|
19
|
+
? html`
|
|
20
|
+
<details affected>
|
|
21
|
+
<summary>${i18next.t('figure.text.show-affected-parts')}</summary>
|
|
22
|
+
<code where>${finding.at}</code>
|
|
23
|
+
</details>
|
|
24
|
+
`
|
|
25
|
+
: nothing}
|
|
26
|
+
${finding.why ? html`<span why-line>${finding.why}</span>` : nothing}
|
|
27
|
+
${finding.how ? html`<span how-line><md-icon>build</md-icon>${finding.how}</span>` : nothing}
|
|
28
|
+
${actions}
|
|
29
|
+
</li>
|
|
30
|
+
`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const gateFindingStyles = css`
|
|
34
|
+
ul[gate] {
|
|
35
|
+
margin: var(--spacing-small, 6px) 0 0;
|
|
36
|
+
padding: 0;
|
|
37
|
+
list-style: none;
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: 3px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
li[head] {
|
|
44
|
+
font: var(--label-font, inherit);
|
|
45
|
+
font-size: 0.72rem;
|
|
46
|
+
color: var(--md-sys-color-error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
li[head][quiet] {
|
|
50
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
li[finding] {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
align-items: flex-start;
|
|
57
|
+
gap: 3px;
|
|
58
|
+
padding: 3px 6px;
|
|
59
|
+
font-size: 0.72rem;
|
|
60
|
+
line-height: 1.5;
|
|
61
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
62
|
+
background-color: var(--md-sys-color-surface-container);
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
li[finding][blocking] {
|
|
67
|
+
color: var(--md-sys-color-on-error-container, var(--md-sys-color-on-surface));
|
|
68
|
+
background-color: var(--md-sys-color-error-container);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
li[finding] code[where] {
|
|
72
|
+
font-family: var(--mono-font, monospace);
|
|
73
|
+
font-size: 0.68rem;
|
|
74
|
+
opacity: 0.85;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
li[finding] details[affected] {
|
|
78
|
+
width: 100%;
|
|
79
|
+
color: inherit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
li[finding] details[affected] summary {
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
font-weight: 600;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* 왜 나쁜지는 곁글로 — 읽어야 하지만 잰 것보다 먼저 눈에 들 필요는 없다 */
|
|
88
|
+
li[finding] span[why-line] {
|
|
89
|
+
opacity: 0.8;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* 무엇을 바꾸면 되나 — 이 줄이 실제로 사람이 할 일이다. 연장 표를 달아 눈에 걸리게 한다 */
|
|
93
|
+
li[finding] span[how-line] {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: flex-start;
|
|
96
|
+
gap: 4px;
|
|
97
|
+
padding-top: 2px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
}
|
|
100
|
+
li[finding] span[how-line] md-icon {
|
|
101
|
+
--md-icon-size: 13px;
|
|
102
|
+
flex: none;
|
|
103
|
+
margin-top: 1px;
|
|
104
|
+
}
|
|
105
|
+
`
|