@things-factory/figure-ui 10.1.75 → 10.1.76

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 (235) hide show
  1. package/client/figure-assistant.ts +3 -3
  2. package/client/graphql/{v3-store.ts → store.ts} +6 -6
  3. package/client/modeller/{v3-ai-proposal.ts → ai-proposal.ts} +8 -8
  4. package/client/modeller/{v3-align.ts → align.ts} +14 -14
  5. package/client/modeller/{v3-canvas.ts → canvas.ts} +13 -13
  6. package/client/modeller/{v3-clips.ts → clips.ts} +5 -5
  7. package/client/modeller/{v3-controls.ts → controls.ts} +2 -2
  8. package/client/modeller/{v3-editor.ts → editor.ts} +117 -117
  9. package/client/modeller/{figure-v3-capability.ts → figure-capability-panel.ts} +7 -7
  10. package/client/modeller/{figure-v3-clips.ts → figure-clips.ts} +10 -10
  11. package/client/modeller/{figure-v3-controls.ts → figure-controls.ts} +9 -9
  12. package/client/modeller/{v3-figure-facts.ts → figure-facts.ts} +11 -11
  13. package/client/modeller/{figure-v3-first-save.ts → figure-first-save.ts} +3 -3
  14. package/client/modeller/figure-history-panel.ts +1 -1
  15. package/client/modeller/{figure-v3-links.ts → figure-links.ts} +35 -35
  16. package/client/modeller/{figure-v3-material.ts → figure-material-panel.ts} +20 -20
  17. package/client/modeller/{figure-v3-parts.ts → figure-parts.ts} +15 -15
  18. package/client/modeller/{figure-v3-preview.ts → figure-preview.ts} +14 -14
  19. package/client/modeller/{figure-v3-properties.ts → figure-properties.ts} +25 -25
  20. package/client/modeller/{figure-v3-recipe-catalog.ts → figure-recipe-catalog.ts} +10 -10
  21. package/client/modeller/{figure-v3-verdict.ts → figure-verdict.ts} +24 -24
  22. package/client/modeller/gate-findings.ts +2 -2
  23. package/client/modeller/{v3-gizmo.ts → gizmo.ts} +11 -11
  24. package/client/modeller/{v3-joint-drag.ts → joint-drag.ts} +11 -11
  25. package/client/modeller/{v3-joint-fan.ts → joint-fan.ts} +13 -13
  26. package/client/modeller/{v3-links.ts → links.ts} +59 -59
  27. package/client/modeller/{v3-live.ts → live.ts} +9 -9
  28. package/client/modeller/{v3-maturity.ts → maturity.ts} +65 -65
  29. package/client/modeller/modeller-route.ts +1 -1
  30. package/client/modeller/{v3-occupancy.ts → occupancy.ts} +6 -6
  31. package/client/modeller/{v3-palette.ts → palette.ts} +3 -3
  32. package/client/modeller/{v3-preview-model.ts → preview-model.ts} +8 -8
  33. package/client/modeller/{v3-product-session.ts → product-session.ts} +25 -25
  34. package/client/modeller/{v3-recipe-source.ts → recipe-source.ts} +2 -2
  35. package/client/modeller/{v3-recipes.ts → recipes.ts} +9 -9
  36. package/client/modeller/{v3-renderer.ts → renderer.ts} +16 -16
  37. package/client/modeller/{v3-stage-model.ts → stage-model.ts} +15 -15
  38. package/client/modeller/{v3-thumbnail.ts → thumbnail.ts} +8 -8
  39. package/client/pages/figure-list-page.ts +4 -4
  40. package/client/pages/{figure-v3-modeller-page.ts → figure-modeller-page.ts} +37 -37
  41. package/client/route.ts +1 -5
  42. package/dist-client/figure-assistant.js +3 -3
  43. package/dist-client/figure-assistant.js.map +1 -1
  44. package/dist-client/graphql/store.d.ts +3 -0
  45. package/dist-client/graphql/store.js +39 -0
  46. package/dist-client/graphql/store.js.map +1 -0
  47. package/dist-client/modeller/ai-proposal.d.ts +51 -0
  48. package/dist-client/modeller/ai-proposal.js +64 -0
  49. package/dist-client/modeller/ai-proposal.js.map +1 -0
  50. package/dist-client/modeller/align.d.ts +30 -0
  51. package/dist-client/modeller/align.js +75 -0
  52. package/dist-client/modeller/align.js.map +1 -0
  53. package/dist-client/modeller/canvas.d.ts +72 -0
  54. package/dist-client/modeller/canvas.js +379 -0
  55. package/dist-client/modeller/canvas.js.map +1 -0
  56. package/dist-client/modeller/clips.d.ts +39 -0
  57. package/dist-client/modeller/clips.js +58 -0
  58. package/dist-client/modeller/clips.js.map +1 -0
  59. package/dist-client/modeller/controls.d.ts +28 -0
  60. package/dist-client/modeller/controls.js +28 -0
  61. package/dist-client/modeller/controls.js.map +1 -0
  62. package/dist-client/modeller/editor.d.ts +203 -0
  63. package/dist-client/modeller/editor.js +1526 -0
  64. package/dist-client/modeller/editor.js.map +1 -0
  65. package/dist-client/modeller/fields.d.ts +45 -0
  66. package/dist-client/modeller/fields.js +83 -0
  67. package/dist-client/modeller/fields.js.map +1 -0
  68. package/dist-client/modeller/figure-capability-panel.d.ts +22 -0
  69. package/dist-client/modeller/figure-capability-panel.js +141 -0
  70. package/dist-client/modeller/figure-capability-panel.js.map +1 -0
  71. package/dist-client/modeller/figure-clips.d.ts +31 -0
  72. package/dist-client/modeller/figure-clips.js +268 -0
  73. package/dist-client/modeller/figure-clips.js.map +1 -0
  74. package/dist-client/modeller/figure-controls.d.ts +36 -0
  75. package/dist-client/modeller/figure-controls.js +226 -0
  76. package/dist-client/modeller/figure-controls.js.map +1 -0
  77. package/dist-client/modeller/figure-facts.d.ts +35 -0
  78. package/dist-client/modeller/figure-facts.js +55 -0
  79. package/dist-client/modeller/figure-facts.js.map +1 -0
  80. package/dist-client/modeller/figure-first-save.d.ts +20 -0
  81. package/dist-client/modeller/figure-first-save.js +118 -0
  82. package/dist-client/modeller/figure-first-save.js.map +1 -0
  83. package/dist-client/modeller/figure-history-panel.d.ts +1 -1
  84. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  85. package/dist-client/modeller/figure-links.d.ts +77 -0
  86. package/dist-client/modeller/figure-links.js +649 -0
  87. package/dist-client/modeller/figure-links.js.map +1 -0
  88. package/dist-client/modeller/figure-material-panel.d.ts +39 -0
  89. package/dist-client/modeller/figure-material-panel.js +323 -0
  90. package/dist-client/modeller/figure-material-panel.js.map +1 -0
  91. package/dist-client/modeller/figure-parts.d.ts +69 -0
  92. package/dist-client/modeller/figure-parts.js +559 -0
  93. package/dist-client/modeller/figure-parts.js.map +1 -0
  94. package/dist-client/modeller/figure-preview.d.ts +34 -0
  95. package/dist-client/modeller/figure-preview.js +346 -0
  96. package/dist-client/modeller/figure-preview.js.map +1 -0
  97. package/dist-client/modeller/figure-properties.d.ts +45 -0
  98. package/dist-client/modeller/figure-properties.js +239 -0
  99. package/dist-client/modeller/figure-properties.js.map +1 -0
  100. package/dist-client/modeller/figure-recipe-catalog.d.ts +38 -0
  101. package/dist-client/modeller/figure-recipe-catalog.js +399 -0
  102. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -0
  103. package/dist-client/modeller/figure-verdict.d.ts +64 -0
  104. package/dist-client/modeller/figure-verdict.js +787 -0
  105. package/dist-client/modeller/figure-verdict.js.map +1 -0
  106. package/dist-client/modeller/first-save.d.ts +17 -0
  107. package/dist-client/modeller/first-save.js +26 -0
  108. package/dist-client/modeller/first-save.js.map +1 -0
  109. package/dist-client/modeller/gate-findings.d.ts +2 -2
  110. package/dist-client/modeller/gate-findings.js +2 -2
  111. package/dist-client/modeller/gate-findings.js.map +1 -1
  112. package/dist-client/modeller/gizmo.d.ts +72 -0
  113. package/dist-client/modeller/gizmo.js +111 -0
  114. package/dist-client/modeller/gizmo.js.map +1 -0
  115. package/dist-client/modeller/help.d.ts +18 -0
  116. package/dist-client/modeller/help.js +37 -0
  117. package/dist-client/modeller/help.js.map +1 -0
  118. package/dist-client/modeller/joint-drag.d.ts +19 -0
  119. package/dist-client/modeller/joint-drag.js +97 -0
  120. package/dist-client/modeller/joint-drag.js.map +1 -0
  121. package/dist-client/modeller/joint-fan.d.ts +9 -0
  122. package/dist-client/modeller/joint-fan.js +276 -0
  123. package/dist-client/modeller/joint-fan.js.map +1 -0
  124. package/dist-client/modeller/keys.d.ts +22 -0
  125. package/dist-client/modeller/keys.js +26 -0
  126. package/dist-client/modeller/keys.js.map +1 -0
  127. package/dist-client/modeller/links.d.ts +267 -0
  128. package/dist-client/modeller/links.js +480 -0
  129. package/dist-client/modeller/links.js.map +1 -0
  130. package/dist-client/modeller/live.d.ts +37 -0
  131. package/dist-client/modeller/live.js +68 -0
  132. package/dist-client/modeller/live.js.map +1 -0
  133. package/dist-client/modeller/maturity.d.ts +120 -0
  134. package/dist-client/modeller/maturity.js +313 -0
  135. package/dist-client/modeller/maturity.js.map +1 -0
  136. package/dist-client/modeller/modeller-route.d.ts +1 -1
  137. package/dist-client/modeller/modeller-route.js +1 -1
  138. package/dist-client/modeller/modeller-route.js.map +1 -1
  139. package/dist-client/modeller/occupancy.d.ts +18 -0
  140. package/dist-client/modeller/occupancy.js +24 -0
  141. package/dist-client/modeller/occupancy.js.map +1 -0
  142. package/dist-client/modeller/palette.d.ts +7 -0
  143. package/dist-client/modeller/palette.js +46 -0
  144. package/dist-client/modeller/palette.js.map +1 -0
  145. package/dist-client/modeller/preview-model.d.ts +47 -0
  146. package/dist-client/modeller/preview-model.js +68 -0
  147. package/dist-client/modeller/preview-model.js.map +1 -0
  148. package/dist-client/modeller/product-session.d.ts +510 -0
  149. package/dist-client/modeller/product-session.js +157 -0
  150. package/dist-client/modeller/product-session.js.map +1 -0
  151. package/dist-client/modeller/recipe-source.d.ts +8 -0
  152. package/dist-client/modeller/recipe-source.js +14 -0
  153. package/dist-client/modeller/recipe-source.js.map +1 -0
  154. package/dist-client/modeller/recipes.d.ts +37 -0
  155. package/dist-client/modeller/recipes.js +103 -0
  156. package/dist-client/modeller/recipes.js.map +1 -0
  157. package/dist-client/modeller/refusals.d.ts +68 -0
  158. package/dist-client/modeller/refusals.js +142 -0
  159. package/dist-client/modeller/refusals.js.map +1 -0
  160. package/dist-client/modeller/renderer.d.ts +40 -0
  161. package/dist-client/modeller/renderer.js +73 -0
  162. package/dist-client/modeller/renderer.js.map +1 -0
  163. package/dist-client/modeller/stage-model.d.ts +39 -0
  164. package/dist-client/modeller/stage-model.js +147 -0
  165. package/dist-client/modeller/stage-model.js.map +1 -0
  166. package/dist-client/modeller/thumbnail.d.ts +3 -0
  167. package/dist-client/modeller/thumbnail.js +75 -0
  168. package/dist-client/modeller/thumbnail.js.map +1 -0
  169. package/dist-client/pages/figure-list-page.d.ts +1 -1
  170. package/dist-client/pages/figure-list-page.js +4 -4
  171. package/dist-client/pages/figure-list-page.js.map +1 -1
  172. package/dist-client/pages/figure-modeller-page.d.ts +102 -0
  173. package/dist-client/pages/figure-modeller-page.js +548 -0
  174. package/dist-client/pages/figure-modeller-page.js.map +1 -0
  175. package/dist-client/route.d.ts +1 -1
  176. package/dist-client/route.js +1 -5
  177. package/dist-client/route.js.map +1 -1
  178. package/dist-client/tsconfig.tsbuildinfo +1 -1
  179. package/docs/product-cutover.md +25 -0
  180. package/package.json +5 -5
  181. package/test/{v3-ai-proposal.test.ts → ai-proposal.test.ts} +6 -6
  182. package/test/{v3-align.test.ts → align.test.ts} +8 -8
  183. package/test/browser/modeller-page.js +30 -30
  184. package/test/browser/modeller.check.mjs +14 -14
  185. package/test/browser/serve.mjs +1 -1
  186. package/test/browser/thumbnail-page.js +6 -6
  187. package/test/{v3-clips.test.ts → clips.test.ts} +18 -18
  188. package/test/{v3-controls.test.ts → controls.test.ts} +15 -15
  189. package/test/{v3-fields.test.ts → fields.test.ts} +13 -13
  190. package/test/figure-assistant-surface.test.ts +3 -3
  191. package/test/{v3-first-save.test.ts → first-save.test.ts} +10 -10
  192. package/test/fixtures/joint-column.json +2 -2
  193. package/test/fixtures/joint-roller.json +2 -2
  194. package/test/fixtures/robot-arm-6dof.json +2 -2
  195. package/test/{v3-gizmo.test.ts → gizmo.test.ts} +33 -33
  196. package/test/{v3-joint-drag.test.ts → joint-drag.test.ts} +17 -17
  197. package/test/{v3-joint-fan.test.ts → joint-fan.test.ts} +18 -18
  198. package/test/joints.ts +6 -6
  199. package/test/{v3-keys.test.ts → keys.test.ts} +2 -2
  200. package/test/{v3-layout.test.ts → layout.test.ts} +8 -8
  201. package/test/{v3-links.test.ts → links.test.ts} +201 -201
  202. package/test/{v3-live.test.ts → live.test.ts} +18 -18
  203. package/test/{v3-material.test.ts → material.test.ts} +12 -12
  204. package/test/{v3-maturity.test.ts → maturity.test.ts} +74 -74
  205. package/test/modeller-route.test.ts +5 -6
  206. package/test/{v3-palette-icons.test.ts → palette-icons.test.ts} +1 -1
  207. package/test/{v3-parts-wiring.test.ts → parts-wiring.test.ts} +5 -5
  208. package/test/{v3-preview.test.ts → preview.test.ts} +12 -12
  209. package/test/{v3-product-session.test.ts → product-session.test.ts} +17 -17
  210. package/test/{v3-product-write.test.ts → product-write.test.ts} +20 -20
  211. package/test/{v3-properties.test.ts → properties.test.ts} +24 -24
  212. package/test/{v3-recipes.test.ts → recipes.test.ts} +9 -9
  213. package/test/{v3-refusals.test.ts → refusals.test.ts} +43 -43
  214. package/test/{v3-release-panel.test.ts → release-panel.test.ts} +21 -21
  215. package/test/renderer.test.ts +24 -0
  216. package/test/resolve-ts-siblings.mjs +1 -1
  217. package/test/{v3-scene-canvas.test.ts → scene-canvas.test.ts} +6 -6
  218. package/test/{v3-stage-model.test.ts → stage-model.test.ts} +5 -5
  219. package/test/{v3-theme.test.ts → theme.test.ts} +4 -4
  220. package/test/{v3-thumbnail.test.ts → thumbnail.test.ts} +4 -4
  221. package/test/{v3-verdict.test.ts → verdict.test.ts} +17 -17
  222. package/test/{v3-view.test.ts → view.test.ts} +2 -2
  223. package/things-factory.config.js +2 -3
  224. package/translations/en.json +2 -2
  225. package/translations/ja.json +2 -2
  226. package/translations/ko.json +2 -2
  227. package/translations/ms.json +2 -2
  228. package/translations/zh.json +2 -2
  229. package/docs/v3-product-cutover.md +0 -25
  230. package/test/v3-renderer.test.ts +0 -24
  231. /package/client/modeller/{v3-fields.ts → fields.ts} +0 -0
  232. /package/client/modeller/{v3-first-save.ts → first-save.ts} +0 -0
  233. /package/client/modeller/{v3-help.ts → help.ts} +0 -0
  234. /package/client/modeller/{v3-keys.ts → keys.ts} +0 -0
  235. /package/client/modeller/{v3-refusals.ts → refusals.ts} +0 -0
@@ -0,0 +1,787 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import '@material/web/icon/icon.js';
3
+ import { css, html, LitElement, nothing, svg } from 'lit';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { i18next, localize } from '@operato/i18n';
6
+ import { ScrollbarStyles } from '@operato/styles';
7
+ import { LIMITS, parseFigureAsset, FIGURE_PER_INSTANCE, figureCostOf } from '@hatiolab/figure-model';
8
+ import { inspectFigureDraft } from '../graphql/index.js';
9
+ import { gateFindingStyles, renderGateFinding } from './gate-findings.js';
10
+ import { MEASURED } from './scene-budget.js';
11
+ import { SidePanelStyles } from './side-panel-styles.js';
12
+ import { budgetOf, CURVE_SIZE, curveLayout, GRADE_SCALE, shortNumber, figureReportOf } from './maturity.js';
13
+ /** How long an edit settles before the server is asked again. One question per pause, not one per keystroke. */
14
+ export const VERDICT_SETTLE_MS = 400;
15
+ /**
16
+ * 성숙도 — the modeller's verdict while editing.
17
+ *
18
+ * It shows, as the author edits: how many fit on one screen with the grade beside it and the budget that
19
+ * decided the grade, the grade scale, six maturity axes as a radar and a list, the budget bars, the drawing cost,
20
+ * the reuse trend, and the findings. An earlier tab kept only the count and the findings (user, 2026-09-24:
21
+ * 「분석부분도 많이 부족하고, 왜 이렇게 너프되었는지」).
22
+ *
23
+ * ## What is computed where
24
+ *
25
+ * The numbers come from `maturity.ts`, which reads the evaluated asset the way the renderer draws it — the
26
+ * screen calls and shows, it does not count. The count uses the same `capacityFrom` the catalogue cards use, so the
27
+ * tab and the card cannot disagree.
28
+ *
29
+ * The findings do not come from here: they come from the server's release door (`inspectFigureDraft`), the same
30
+ * door and the same sentences as the release panel. A second judgement on the screen would say something different
31
+ * the first time either changed. The release judgement is the server's, so that section is the server's answer.
32
+ *
33
+ * ## The grade
34
+ *
35
+ * figure-model's `figureScoreOf`, which the catalogue uses too: headroom against placing `LIMITS.screenInstances` of the
36
+ * figure on one screen (chief architect, 2026-09-24) -- the screen's measured groups and its triangle target shared
37
+ * per instance -- and the rule limits for independently moved parts and transparent materials, decided by the
38
+ * tightest. The two budgets has no field for are left out and the tab says so.
39
+ */
40
+ /** A per-instance share of the screen is not a whole number; what an instance may draw is the whole part of it. */
41
+ const limitOf = (use) => Math.floor(use.limit).toLocaleString();
42
+ let FigureVerdict = class FigureVerdict extends localize(i18next)(LitElement) {
43
+ constructor() {
44
+ super(...arguments);
45
+ /** Bodies in the asset, drawn or hidden. With none there is nothing to fit the space to, so the button is not offered. */
46
+ this.bodies = 0;
47
+ /** Whether the trend chart is open at its own size. */
48
+ this.expanded = false;
49
+ this.failure = '';
50
+ this.asking = false;
51
+ /** Answers can come back out of order; only the newest question's answer is shown. */
52
+ this.asked = 0;
53
+ this.fit = () => {
54
+ this.dispatchEvent(new CustomEvent('fit-base-to-parts', { bubbles: true, composed: true }));
55
+ };
56
+ }
57
+ static { this.styles = [
58
+ ScrollbarStyles,
59
+ SidePanelStyles,
60
+ gateFindingStyles,
61
+ css `
62
+ :host {
63
+ display: flex;
64
+ flex-direction: column;
65
+ overflow-y: auto;
66
+ }
67
+ section p {
68
+ margin: 0;
69
+ }
70
+
71
+ /* ── How many fit ───────────────────────────────────────────────
72
+ The value of low poly is how many can stand on one screen, so that answer leads. The grade says whether
73
+ the shape keeps inside its declared limits, so it is the small badge beside it. */
74
+ p[lead],
75
+ p[basis],
76
+ p[quiet] {
77
+ font-size: 0.74rem;
78
+ line-height: 1.55;
79
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
80
+ }
81
+ p[basis] em {
82
+ font-style: normal;
83
+ color: var(--md-sys-color-tertiary);
84
+ }
85
+ div[verdict] {
86
+ display: flex;
87
+ align-items: baseline;
88
+ gap: 6px;
89
+ }
90
+ span[capacity] {
91
+ font-size: 2.1rem;
92
+ font-weight: 800;
93
+ line-height: 1;
94
+ letter-spacing: -0.01em;
95
+ color: var(--md-sys-color-primary);
96
+ }
97
+ span[unit] {
98
+ font-size: 0.9rem;
99
+ font-weight: 600;
100
+ color: var(--md-sys-color-primary);
101
+ }
102
+ /* The badge says what it measures (headroom), so its number is not read as a score out of 100. */
103
+ span[grade] {
104
+ display: flex;
105
+ align-items: baseline;
106
+ gap: 5px;
107
+ margin-left: auto;
108
+ padding: 3px 10px;
109
+ font-size: 0.76rem;
110
+ font-weight: 700;
111
+ color: var(--md-sys-color-on-primary-container);
112
+ background-color: var(--md-sys-color-primary-container);
113
+ border-radius: 999em;
114
+ }
115
+ span[grade] small {
116
+ display: inline;
117
+ margin: 0;
118
+ font-size: 0.68rem;
119
+ font-weight: 500;
120
+ color: inherit;
121
+ }
122
+ /* C keeps within the limit. Red is for over it (D) only; red on a kept limit asks for a fix nothing needs. */
123
+ span[grade][tight] {
124
+ color: var(--md-sys-color-on-tertiary-container);
125
+ background-color: var(--md-sys-color-tertiary-container);
126
+ }
127
+ span[grade][over] {
128
+ color: var(--md-sys-color-on-error-container);
129
+ background-color: var(--md-sys-color-error-container);
130
+ }
131
+ p[because] {
132
+ font-size: 0.78rem;
133
+ line-height: 1.6;
134
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
135
+ }
136
+ p[because] strong {
137
+ color: var(--md-sys-color-on-surface);
138
+ }
139
+ /* The grade scale. Not folded away: it is compared against every time, not learned once. */
140
+ p[scale] {
141
+ display: flex;
142
+ flex-wrap: wrap;
143
+ gap: 2px 10px;
144
+ font-size: 0.7rem;
145
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
146
+ }
147
+ p[scale] b {
148
+ font-weight: 700;
149
+ }
150
+ p[scale] span[now] {
151
+ color: var(--md-sys-color-on-surface);
152
+ }
153
+ p[scale] span[now] b {
154
+ color: var(--md-sys-color-primary);
155
+ }
156
+
157
+ /* ── Maturity radar ─────────────────────────────────────────────
158
+ Only the measured axes are drawn; an unmeasured one drawn as a spoke reads as a dent, which is a zero. */
159
+ div[radar] {
160
+ display: flex;
161
+ justify-content: center;
162
+ padding: 2px 0 6px 0;
163
+ color: var(--md-sys-color-primary);
164
+ }
165
+ div[radar] svg {
166
+ width: 100%;
167
+ max-width: 240px;
168
+ height: auto;
169
+ }
170
+ div[radar] polygon[web] {
171
+ fill: none;
172
+ stroke: var(--md-sys-color-outline-variant);
173
+ stroke-width: 0.6;
174
+ }
175
+ div[radar] line[spoke] {
176
+ stroke: var(--md-sys-color-outline-variant);
177
+ stroke-width: 0.6;
178
+ }
179
+ div[radar] polygon[shape] {
180
+ fill: color-mix(in srgb, var(--md-sys-color-primary) 22%, transparent);
181
+ stroke: var(--md-sys-color-primary);
182
+ stroke-width: 1.4;
183
+ stroke-linejoin: round;
184
+ }
185
+ div[radar] circle[dot] {
186
+ fill: var(--md-sys-color-primary);
187
+ }
188
+ div[radar] text {
189
+ font-size: 5.2px;
190
+ fill: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
191
+ }
192
+ p[unmeasured] {
193
+ font-size: 0.7rem;
194
+ line-height: 1.5;
195
+ color: var(--md-sys-color-tertiary);
196
+ }
197
+
198
+ /* ── Axes and budgets ───────────────────────────────────────────
199
+ Not merged into one number: merged, 「what to fix」 disappears. */
200
+ ul[axes],
201
+ ul[budgets] {
202
+ margin: 0;
203
+ padding: 0;
204
+ list-style: none;
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 7px;
208
+ }
209
+ ul[axes] li,
210
+ ul[budgets] li {
211
+ display: grid;
212
+ grid-template-columns: 1fr auto;
213
+ gap: 2px 8px;
214
+ font-size: 0.75rem;
215
+ }
216
+ ul[axes] li[blocked] {
217
+ opacity: 0.7;
218
+ }
219
+ ul[axes] em {
220
+ grid-column: 1 / -1;
221
+ font-style: normal;
222
+ font-size: 0.68rem;
223
+ line-height: 1.5;
224
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
225
+ }
226
+ ul[axes] li[blocked] em {
227
+ color: var(--md-sys-color-tertiary);
228
+ }
229
+ span[reading] {
230
+ font-family: var(--mono-font, monospace);
231
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
232
+ }
233
+ div[bar] {
234
+ grid-column: 1 / -1;
235
+ height: 4px;
236
+ background-color: var(--md-sys-color-surface-container-highest);
237
+ border-radius: 2px;
238
+ overflow: hidden;
239
+ }
240
+ div[bar] > i {
241
+ display: block;
242
+ height: 100%;
243
+ background-color: var(--md-sys-color-primary);
244
+ }
245
+ div[bar] > i[tight] {
246
+ background-color: var(--md-sys-color-error);
247
+ }
248
+
249
+ /* ── Drawing cost ─────────────────────────────────────────────── */
250
+ dl[metrics] {
251
+ margin: 0;
252
+ display: grid;
253
+ grid-template-columns: 1fr auto;
254
+ gap: 4px 8px;
255
+ font-size: 0.78rem;
256
+ }
257
+ dl[metrics] dt {
258
+ color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
259
+ }
260
+ dl[metrics] dd {
261
+ margin: 0;
262
+ text-align: right;
263
+ font-family: var(--mono-font, monospace);
264
+ }
265
+
266
+ /* ── Trend ──────────────────────────────────────────────────────
267
+ The chart's natural size is 680px; in a 300px side its words are 4px. The side holds a button, the chart
268
+ opens at its own size. */
269
+ button[expand] {
270
+ justify-self: start;
271
+ padding: 4px 10px;
272
+ font: inherit;
273
+ font-size: 0.74rem;
274
+ color: var(--md-sys-color-on-surface);
275
+ background: none;
276
+ border: 1px solid var(--md-sys-color-outline-variant);
277
+ border-radius: 6px;
278
+ cursor: pointer;
279
+ }
280
+ button[expand]:hover {
281
+ background-color: var(--md-sys-color-surface-container);
282
+ }
283
+ div[sheet] {
284
+ position: fixed;
285
+ inset: 0;
286
+ z-index: 10;
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+ padding: 12px;
291
+ background-color: color-mix(in srgb, var(--md-sys-color-scrim, black) 32%, transparent);
292
+ }
293
+ div[sheet] > figure {
294
+ max-width: 100%;
295
+ max-height: 100%;
296
+ margin: 0;
297
+ padding: 12px;
298
+ overflow: auto;
299
+ color: var(--md-sys-color-on-surface);
300
+ background-color: var(--md-sys-color-surface-container-lowest);
301
+ border-radius: 6px;
302
+ box-shadow: 0 8px 32px color-mix(in srgb, var(--md-sys-color-shadow, black) 28%, transparent);
303
+ }
304
+ div[sheet] svg {
305
+ display: block;
306
+ /* Not shrunk below its natural size — a narrow window scrolls instead. */
307
+ min-width: 680px;
308
+ font-family: inherit;
309
+ font-size: 11px;
310
+ fill: currentColor;
311
+ }
312
+ div[sheet] svg [grid] {
313
+ stroke: currentColor;
314
+ stroke-opacity: 0.12;
315
+ }
316
+ div[sheet] svg [muted] {
317
+ opacity: 0.55;
318
+ }
319
+ div[sheet] svg path[series] {
320
+ fill: none;
321
+ stroke-width: 2;
322
+ stroke-linejoin: round;
323
+ }
324
+ div[sheet] svg [series='triangles'] {
325
+ stroke: var(--md-sys-color-error);
326
+ fill: var(--md-sys-color-error);
327
+ }
328
+ div[sheet] svg [series='drawCalls'] {
329
+ stroke: var(--md-sys-color-tertiary);
330
+ fill: var(--md-sys-color-tertiary);
331
+ }
332
+ div[sheet] svg [series='geometries'] {
333
+ stroke: var(--md-sys-color-primary);
334
+ fill: var(--md-sys-color-primary);
335
+ }
336
+ div[sheet] svg [series='materials'] {
337
+ stroke: var(--md-sys-color-secondary);
338
+ fill: var(--md-sys-color-secondary);
339
+ }
340
+ div[sheet] svg path[series],
341
+ div[sheet] svg line[series] {
342
+ fill: none;
343
+ }
344
+ /* A legend value is written in its line's colour, not outlined in it. */
345
+ div[sheet] svg text[series] {
346
+ stroke: none;
347
+ }
348
+ div[sheet] figcaption {
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: flex-end;
352
+ }
353
+ div[sheet] button[close] {
354
+ padding: 5px 14px;
355
+ font: inherit;
356
+ color: var(--md-sys-color-on-surface);
357
+ background: none;
358
+ border: 1px solid var(--md-sys-color-outline-variant);
359
+ border-radius: 999em;
360
+ cursor: pointer;
361
+ }
362
+
363
+ /* ── Release verdict ───────────────────────────────────────────── */
364
+ p[clear] {
365
+ color: var(--md-sys-color-primary);
366
+ font-weight: 600;
367
+ }
368
+ p[failed] {
369
+ color: var(--md-sys-color-error);
370
+ }
371
+ button[fit] {
372
+ align-self: flex-start;
373
+ margin-top: 4px;
374
+ padding: 4px 10px;
375
+ font: inherit;
376
+ color: var(--md-sys-color-on-secondary-container);
377
+ background: var(--md-sys-color-secondary-container);
378
+ border: none;
379
+ border-radius: 999em;
380
+ cursor: pointer;
381
+ }
382
+ `
383
+ ]; }
384
+ disconnectedCallback() {
385
+ clearTimeout(this.timer);
386
+ super.disconnectedCallback();
387
+ }
388
+ updated(changes) {
389
+ if (changes.has('source'))
390
+ this.measure();
391
+ if (changes.has('source') || changes.has('figureId') || changes.has('figureType'))
392
+ this.schedule();
393
+ }
394
+ measure() {
395
+ let report;
396
+ let bodies = 0;
397
+ try {
398
+ const asset = this.source ? parseFigureAsset(this.source) : undefined;
399
+ const cost = asset ? figureCostOf(asset) : undefined;
400
+ bodies = cost ? cost.groups + cost.hidden : 0;
401
+ report = asset ? figureReportOf(asset) : undefined;
402
+ }
403
+ catch {
404
+ /* A source the format refuses has no report; the server's finding below says why. */
405
+ report = undefined;
406
+ }
407
+ this.report = report;
408
+ this.bodies = bodies;
409
+ if (!report)
410
+ this.expanded = false;
411
+ this.dispatchEvent(new CustomEvent('capacity-changed', { detail: { instances: report?.capacity.instances, budget: budgetOf(report) } }));
412
+ }
413
+ schedule() {
414
+ clearTimeout(this.timer);
415
+ if (!this.source) {
416
+ this.inspection = undefined;
417
+ return;
418
+ }
419
+ this.timer = setTimeout(() => this.ask(), VERDICT_SETTLE_MS);
420
+ }
421
+ async ask() {
422
+ const question = ++this.asked;
423
+ const source = this.source;
424
+ this.asking = true;
425
+ try {
426
+ const inspection = await inspectFigureDraft(source, this.figureId);
427
+ if (question !== this.asked)
428
+ return;
429
+ this.inspection = inspection;
430
+ this.failure = '';
431
+ }
432
+ catch (error) {
433
+ if (question !== this.asked)
434
+ return;
435
+ /* The server's reason as it gave it — the screen does not make one up. The last answer is dropped with it. */
436
+ this.inspection = undefined;
437
+ this.failure = error instanceof Error ? error.message : String(error);
438
+ }
439
+ finally {
440
+ if (question === this.asked)
441
+ this.asking = false;
442
+ }
443
+ }
444
+ render() {
445
+ /*
446
+ With nothing drawn the report is guidance, not a verdict: the sections below have meaning only over parts,
447
+ and a number drawn from nothing reads as zero, which reads as bad. The release verdict stays — a new figure
448
+ is already refused for its undeclared space, and the way out is offered there.
449
+ */
450
+ const report = this.report;
451
+ if (!report) {
452
+ return html `
453
+ <section>
454
+ <h3>${i18next.t('figure.label.maturity')}</h3>
455
+ <p quiet>${i18next.t('figure.text.report-appears-when-you-add-a-part')}</p>
456
+ </section>
457
+ ${this.renderGate()}
458
+ `;
459
+ }
460
+ return html `
461
+ ${this.renderVerdict(report)} ${this.renderAxes(report)} ${this.renderBudgets(report)} ${this.renderMetrics(report)}
462
+ ${this.renderCurve()} ${this.renderGate()} ${this.renderSheet(report)}
463
+ `;
464
+ }
465
+ /**
466
+ * The head — how many fit, with the grade beside it, the budget that decided it, the scale, and the basis.
467
+ *
468
+ * The count's denominator is measured (2026-09-18, Apple M5) and says so with the machine and the date; the
469
+ * triangle side is still the design document's target and says that.
470
+ */
471
+ renderVerdict(report) {
472
+ const { capacity, score } = report;
473
+ return html `
474
+ <section>
475
+ <h3>${i18next.t('figure.label.maturity')}</h3>
476
+ <p lead>${i18next.t('figure.label.axis-scale')} — ${i18next.t('figure.text.how-many-fit')}</p>
477
+ <div verdict>
478
+ <span capacity>${capacity.instances.toLocaleString()}</span>
479
+ <span unit>${i18next.t('figure.text.instances-unit')}</span>
480
+ <span
481
+ grade
482
+ ?tight=${score.grade === 'C'}
483
+ ?over=${score.grade === 'D'}
484
+ title=${i18next.t('figure.text.grade-explained', { target: LIMITS.screenInstances })}
485
+ >
486
+ ${score.grade}
487
+ <small>${i18next.t('figure.text.headroom-percent', { percent: score.score })}</small>
488
+ </span>
489
+ </div>
490
+ <p because>
491
+ ${i18next.t('figure.text.grade-decided-by')}
492
+ <strong>${i18next.t('figure.label.budget-' + score.tightest.key)}</strong>
493
+ — ${score.tightest.used.toLocaleString()} / ${limitOf(score.tightest)} (${Math.round(score.tightest.ratio * 100)}%)
494
+ </p>
495
+ <p basis>
496
+ ${i18next.t('figure.text.grade-target', {
497
+ target: LIMITS.screenInstances,
498
+ groups: Math.floor(FIGURE_PER_INSTANCE.groups),
499
+ triangles: Math.floor(FIGURE_PER_INSTANCE.triangles).toLocaleString()
500
+ })}
501
+ </p>
502
+ ${this.renderGradeScale(score.grade)}
503
+ <p basis>
504
+ ${i18next.t('figure.text.bound-by-' + capacity.bound, {
505
+ per: capacity.per.toLocaleString(),
506
+ budget: capacity.budget.toLocaleString()
507
+ })}
508
+ <em>
509
+ ${capacity.bound === 'groups'
510
+ ? i18next.t('figure.text.budget-measured-on', { gpu: MEASURED.gpu, on: MEASURED.on })
511
+ : i18next.t('figure.text.budget-not-measured-yet')}
512
+ </em>
513
+ </p>
514
+ </section>
515
+ `;
516
+ }
517
+ /** A · B · C · D and where this figure stands, drawn from the bounds the grade is computed with. */
518
+ renderGradeScale(now) {
519
+ const bounds = [...GRADE_SCALE.map(([grade, bound]) => [grade, `≤ ${bound * 100}%`]), ['D', '> 100%']];
520
+ return html `
521
+ <p scale>
522
+ <span>${i18next.t('figure.text.grade-scale-lead')}</span>
523
+ ${bounds.map(([grade, bound]) => html `<span ?now=${grade === now}><b>${grade}</b> ${bound}</span>`)}
524
+ </p>
525
+ `;
526
+ }
527
+ /**
528
+ * Maturity by axis — a radar of the measured axes, a line naming the unmeasured ones, and the list.
529
+ *
530
+ * Scale is left out of the list: it leads the tab, and the same number twice reads as two different things.
531
+ */
532
+ renderAxes(report) {
533
+ const measured = report.axes.filter(axis => axis.ratio !== undefined);
534
+ const unmeasured = report.axes.filter(axis => axis.ratio === undefined);
535
+ return html `
536
+ <section>
537
+ <h3>${i18next.t('figure.label.by-axis')}</h3>
538
+ <div radar>${this.renderRadar(measured)}</div>
539
+ ${unmeasured.length
540
+ ? html `<p unmeasured>
541
+ ${i18next.t('figure.text.axes-not-measurable-yet', {
542
+ axes: unmeasured.map(axis => i18next.t('figure.label.axis-' + axis.key)).join(' · ')
543
+ })}
544
+ </p>`
545
+ : nothing}
546
+ <ul axes>
547
+ ${report.axes.filter(axis => axis.key !== 'scale').map(axis => this.renderAxis(axis))}
548
+ </ul>
549
+ </section>
550
+ `;
551
+ }
552
+ /** The radar: rings at a third, two thirds and full, short names so the left edge is not cut. */
553
+ renderRadar(axes) {
554
+ const cx = 60;
555
+ const cy = 46;
556
+ const r = 28;
557
+ const at = (index, ratio) => {
558
+ const angle = -Math.PI / 2 + (index * 2 * Math.PI) / axes.length;
559
+ return [cx + r * ratio * Math.cos(angle), cy + r * ratio * Math.sin(angle)];
560
+ };
561
+ const clamp = (ratio) => Math.max(0.04, Math.min(1, ratio ?? 0));
562
+ const points = axes.map((axis, index) => at(index, clamp(axis.ratio)).map(n => n.toFixed(1)).join(',')).join(' ');
563
+ return svg `
564
+ <svg viewBox="0 0 120 96" aria-hidden="true">
565
+ ${[0.33, 0.66, 1].map(ring => svg `<polygon web points=${axes.map((_, i) => at(i, ring).map(n => n.toFixed(1)).join(',')).join(' ')}></polygon>`)}
566
+ ${axes.map((_, index) => {
567
+ const [x, y] = at(index, 1);
568
+ return svg `<line spoke x1=${cx} y1=${cy} x2=${x.toFixed(1)} y2=${y.toFixed(1)}></line>`;
569
+ })}
570
+ <polygon shape points=${points}></polygon>
571
+ ${axes.map((axis, index) => {
572
+ const [x, y] = at(index, clamp(axis.ratio));
573
+ return svg `<circle dot cx=${x.toFixed(1)} cy=${y.toFixed(1)} r="1.4"></circle>`;
574
+ })}
575
+ ${axes.map((axis, index) => {
576
+ const [x, y] = at(index, 1.28);
577
+ const anchor = x < cx - 3 ? 'end' : x > cx + 3 ? 'start' : 'middle';
578
+ return svg `<text x=${x.toFixed(1)} y=${(y + 1.8).toFixed(1)} text-anchor=${anchor}>${i18next.t('figure.label.axis-' + axis.key + '-short')}</text>`;
579
+ })}
580
+ </svg>
581
+ `;
582
+ }
583
+ renderAxis(axis) {
584
+ const name = i18next.t('figure.label.axis-' + axis.key);
585
+ if (axis.blocked) {
586
+ return html `
587
+ <li blocked>
588
+ <span>${name}</span>
589
+ <span reading>${i18next.t('figure.text.not-measurable-yet')}</span>
590
+ <em>${i18next.t(axis.blocked)}</em>
591
+ </li>
592
+ `;
593
+ }
594
+ const ratio = Math.max(0, Math.min(1, axis.ratio ?? 0));
595
+ return html `
596
+ <li>
597
+ <span>${name}</span>
598
+ <span reading>${axis.readingKey ? i18next.t(axis.readingKey) : (axis.reading ?? '')}</span>
599
+ <div bar><i style="width:${Math.round(ratio * 100)}%"></i></div>
600
+ ${axis.because ? html `<em>${i18next.t(axis.because)}</em>` : nothing}
601
+ </li>
602
+ `;
603
+ }
604
+ /** The budget bars. The one closest to its limit is coloured — it decided the grade. */
605
+ renderBudgets(report) {
606
+ const { score } = report;
607
+ return html `
608
+ <section>
609
+ <h3>${i18next.t('figure.label.budgets')}</h3>
610
+ <ul budgets>
611
+ ${score.budgets.map(use => html `
612
+ <li>
613
+ <span>${i18next.t('figure.label.budget-' + use.key)}</span>
614
+ <span>${use.used.toLocaleString()} / ${limitOf(use)}</span>
615
+ <div bar><i ?tight=${use.key === score.tightest.key} style="width:${Math.round(Math.min(1, use.ratio) * 100)}%"></i></div>
616
+ </li>
617
+ `)}
618
+ </ul>
619
+ <p quiet>${i18next.t('figure.text.budgets-not-in')}</p>
620
+ </section>
621
+ `;
622
+ }
623
+ renderMetrics(report) {
624
+ return html `
625
+ <section>
626
+ <h3>${i18next.t('figure.label.weight')}</h3>
627
+ <dl metrics>
628
+ <dt>${i18next.t('figure.label.triangles')}</dt>
629
+ <dd>${report.cost.triangles.toLocaleString()}</dd>
630
+ <dt>${i18next.t('figure.label.distinct-shapes')}</dt>
631
+ <dd>${report.distinctShapes}</dd>
632
+ <dt>${i18next.t('figure.label.distinct-materials')}</dt>
633
+ <dd>${report.distinctMaterials}</dd>
634
+ </dl>
635
+ </section>
636
+ `;
637
+ }
638
+ renderCurve() {
639
+ return html `
640
+ <section>
641
+ <h3>${i18next.t('figure.label.reuse-trend')}</h3>
642
+ <button expand @click=${() => (this.expanded = true)}>${i18next.t('figure.button.see-the-trend-chart')}</button>
643
+ </section>
644
+ `;
645
+ }
646
+ /**
647
+ * The trend at its own size: both axes logarithmic, the growing lines against the flat ones, and a
648
+ * legend with each line's value at the last count.
649
+ */
650
+ renderSheet(report) {
651
+ if (!this.expanded)
652
+ return nothing;
653
+ const layout = curveLayout(report.curve);
654
+ if (!layout)
655
+ return nothing;
656
+ const { width, height, pad } = CURVE_SIZE;
657
+ const plotW = width - pad.left - pad.right;
658
+ const plotH = height - pad.top - pad.bottom;
659
+ const last = report.curve[report.curve.length - 1];
660
+ const label = (key) => i18next.t({
661
+ triangles: 'figure.label.triangles',
662
+ drawCalls: 'figure.label.draw-calls',
663
+ geometries: 'figure.label.vertex-buffers',
664
+ materials: 'figure.label.distinct-materials'
665
+ }[key]);
666
+ return html `
667
+ <div sheet @click=${() => (this.expanded = false)}>
668
+ <figure @click=${(e) => e.stopPropagation()}>
669
+ ${svg `
670
+ <svg viewBox="0 0 ${width} ${height}" width=${width} height=${height} role="img">
671
+ <text x=${pad.left} y="22" font-size="13" font-weight="600">${i18next.t('figure.text.trend-chart-title')}</text>
672
+ <text x=${pad.left} y="38" muted>${i18next.t('figure.text.trend-chart-lead')}</text>
673
+ ${layout.yTicks.map(tick => svg `
674
+ <line grid x1=${pad.left} y1=${tick.y.toFixed(1)} x2=${(pad.left + plotW).toFixed(1)} y2=${tick.y.toFixed(1)}></line>
675
+ <text muted x=${pad.left - 8} y=${(tick.y + 3.5).toFixed(1)} text-anchor="end">${tick.label}</text>
676
+ `)}
677
+ ${layout.xTicks.map(tick => svg `
678
+ <line grid x1=${tick.x.toFixed(1)} y1=${pad.top} x2=${tick.x.toFixed(1)} y2=${(pad.top + plotH).toFixed(1)}></line>
679
+ <text muted x=${tick.x.toFixed(1)} y=${(pad.top + plotH + 16).toFixed(1)} text-anchor="middle">${tick.label}</text>
680
+ `)}
681
+ <text muted x=${(pad.left + plotW / 2).toFixed(1)} y=${height - 8} text-anchor="middle">${i18next.t('figure.label.instance-count')}</text>
682
+ ${layout.lines.map(line => svg `<path series=${line.key} d=${line.d} stroke-dasharray=${line.dashed ? '3 7' : 'none'}></path>`)}
683
+ <g transform="translate(${width - pad.right + 14}, ${pad.top - 6})">
684
+ <text x="0" y="0" font-weight="600">${i18next.t('figure.text.trend-at-count', { n: shortNumber(last.instances) })}</text>
685
+ ${layout.lines.map((line, i) => {
686
+ const y = 24 + i * 34;
687
+ return svg `
688
+ <line series=${line.key} x1="0" y1=${y - 4} x2="18" y2=${y - 4} stroke-width="2" stroke-dasharray=${line.dashed ? '3 7' : 'none'}></line>
689
+ <text x="24" y=${y}>${label(line.key)}</text>
690
+ <text series=${line.key} x="24" y=${y + 12} font-weight="600">
691
+ ${line.last.toLocaleString()}${line.flat ? ` · ${i18next.t('figure.text.trend-stays')}` : ''}
692
+ </text>
693
+ `;
694
+ })}
695
+ ${layout.overlap !== undefined
696
+ ? svg `<text muted x="0" y=${24 + layout.lines.length * 34 + 6}>${i18next.t('figure.text.trend-flat-lines-overlap', { value: layout.overlap.toLocaleString() })}</text>`
697
+ : nothing}
698
+ </g>
699
+ </svg>
700
+ `}
701
+ <figcaption>
702
+ <button close @click=${() => (this.expanded = false)}>${i18next.t('figure.button.close')}</button>
703
+ </figcaption>
704
+ </figure>
705
+ </div>
706
+ `;
707
+ }
708
+ renderGate() {
709
+ const findings = this.inspection?.findings ?? [];
710
+ const blocking = findings.filter(finding => finding.blocking);
711
+ const advisory = findings.filter(finding => !finding.blocking);
712
+ return html `
713
+ <section>
714
+ <h3>${i18next.t('figure.label.release-verdict')}</h3>
715
+ ${this.failure
716
+ ? html `<p failed role="alert">${i18next.t('figure.text.verdict-not-received', { reason: this.failure })}</p>`
717
+ : !this.inspection
718
+ ? html `<p quiet>${i18next.t(this.asking ? 'figure.text.verdict-asking' : 'figure.text.verdict-waiting')}</p>`
719
+ : blocking.length
720
+ ? html `<p>${i18next.t('figure.text.cannot-release-now')}</p>`
721
+ : html `<p clear>${i18next.t('figure.text.nothing-blocks-release')}</p>`}
722
+ ${blocking.length
723
+ ? html `<ul gate>
724
+ <li head>${i18next.t('figure.text.must-fix-to-release')}</li>
725
+ ${blocking.map(finding => renderGateFinding(finding, true, this.actionsFor(finding.code)))}
726
+ </ul>`
727
+ : nothing}
728
+ ${advisory.length
729
+ ? html `<ul gate>
730
+ <li head quiet>${i18next.t('figure.text.does-not-block')}</li>
731
+ ${advisory.map(finding => renderGateFinding(finding, false))}
732
+ </ul>`
733
+ : nothing}
734
+ </section>
735
+ `;
736
+ }
737
+ /** The same way out the release panel offers for the same two findings — declare the space from the parts. */
738
+ actionsFor(code) {
739
+ if (code !== 'part-outside-occupancy' && code !== 'occupancy-undeclared')
740
+ return nothing;
741
+ /* A new figure with no parts is refused as undeclared too, but fitting needs parts to measure (seen 2026-09-24). */
742
+ if (!this.bodies)
743
+ return nothing;
744
+ return html `<button fit @click=${this.fit}>${i18next.t('figure.button.fit-base-to-parts')}</button>`;
745
+ }
746
+ };
747
+ __decorate([
748
+ property({ type: String }),
749
+ __metadata("design:type", String)
750
+ ], FigureVerdict.prototype, "source", void 0);
751
+ __decorate([
752
+ property({ type: String }),
753
+ __metadata("design:type", String)
754
+ ], FigureVerdict.prototype, "figureId", void 0);
755
+ __decorate([
756
+ property({ type: String }),
757
+ __metadata("design:type", String)
758
+ ], FigureVerdict.prototype, "figureType", void 0);
759
+ __decorate([
760
+ state(),
761
+ __metadata("design:type", Object)
762
+ ], FigureVerdict.prototype, "report", void 0);
763
+ __decorate([
764
+ state(),
765
+ __metadata("design:type", Object)
766
+ ], FigureVerdict.prototype, "bodies", void 0);
767
+ __decorate([
768
+ state(),
769
+ __metadata("design:type", Object)
770
+ ], FigureVerdict.prototype, "expanded", void 0);
771
+ __decorate([
772
+ state(),
773
+ __metadata("design:type", Object)
774
+ ], FigureVerdict.prototype, "inspection", void 0);
775
+ __decorate([
776
+ state(),
777
+ __metadata("design:type", Object)
778
+ ], FigureVerdict.prototype, "failure", void 0);
779
+ __decorate([
780
+ state(),
781
+ __metadata("design:type", Object)
782
+ ], FigureVerdict.prototype, "asking", void 0);
783
+ FigureVerdict = __decorate([
784
+ customElement('figure-verdict')
785
+ ], FigureVerdict);
786
+ export { FigureVerdict };
787
+ //# sourceMappingURL=figure-verdict.js.map