@things-factory/figure-ui 10.1.46 → 10.1.47

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 (138) hide show
  1. package/client/modeller/angle-dial.ts +70 -0
  2. package/client/modeller/figure-settings.ts +328 -0
  3. package/client/modeller/figure-v3-capability.ts +137 -0
  4. package/client/modeller/figure-v3-clips.ts +253 -0
  5. package/client/modeller/figure-v3-controls.ts +212 -0
  6. package/client/modeller/figure-v3-links.ts +61 -81
  7. package/client/modeller/figure-v3-material.ts +108 -35
  8. package/client/modeller/figure-v3-parts.ts +96 -3
  9. package/client/modeller/figure-v3-preview.ts +326 -0
  10. package/client/modeller/figure-v3-properties.ts +29 -57
  11. package/client/modeller/figure-v3-verdict.ts +598 -60
  12. package/client/modeller/figure-view.ts +152 -0
  13. package/client/modeller/side-panel-styles.ts +326 -0
  14. package/client/modeller/v3-canvas.ts +221 -263
  15. package/client/modeller/v3-clips.ts +89 -0
  16. package/client/modeller/v3-controls.ts +54 -0
  17. package/client/modeller/v3-editor.ts +531 -223
  18. package/client/modeller/v3-fields.ts +62 -0
  19. package/client/modeller/v3-help.ts +41 -0
  20. package/client/modeller/v3-keys.ts +34 -0
  21. package/client/modeller/v3-links.ts +93 -9
  22. package/client/modeller/v3-live.ts +79 -0
  23. package/client/modeller/v3-maturity.ts +415 -0
  24. package/client/modeller/v3-palette.ts +45 -0
  25. package/client/modeller/v3-preview-model.ts +54 -0
  26. package/client/modeller/v3-stage-model.ts +159 -0
  27. package/client/pages/figure-list-page.ts +4 -4
  28. package/client/pages/figure-v3-modeller-page.ts +66 -134
  29. package/dist-client/modeller/angle-dial.d.ts +32 -0
  30. package/dist-client/modeller/angle-dial.js +41 -0
  31. package/dist-client/modeller/angle-dial.js.map +1 -0
  32. package/dist-client/modeller/figure-settings.d.ts +39 -0
  33. package/dist-client/modeller/figure-settings.js +317 -0
  34. package/dist-client/modeller/figure-settings.js.map +1 -0
  35. package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
  36. package/dist-client/modeller/figure-v3-capability.js +141 -0
  37. package/dist-client/modeller/figure-v3-capability.js.map +1 -0
  38. package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
  39. package/dist-client/modeller/figure-v3-clips.js +268 -0
  40. package/dist-client/modeller/figure-v3-clips.js.map +1 -0
  41. package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
  42. package/dist-client/modeller/figure-v3-controls.js +226 -0
  43. package/dist-client/modeller/figure-v3-controls.js.map +1 -0
  44. package/dist-client/modeller/figure-v3-links.d.ts +5 -2
  45. package/dist-client/modeller/figure-v3-links.js +64 -82
  46. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  47. package/dist-client/modeller/figure-v3-material.d.ts +5 -2
  48. package/dist-client/modeller/figure-v3-material.js +109 -35
  49. package/dist-client/modeller/figure-v3-material.js.map +1 -1
  50. package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
  51. package/dist-client/modeller/figure-v3-parts.js +88 -3
  52. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  53. package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
  54. package/dist-client/modeller/figure-v3-preview.js +322 -0
  55. package/dist-client/modeller/figure-v3-preview.js.map +1 -0
  56. package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
  57. package/dist-client/modeller/figure-v3-properties.js +29 -57
  58. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  59. package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
  60. package/dist-client/modeller/figure-v3-verdict.js +579 -59
  61. package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
  62. package/dist-client/modeller/figure-view.d.ts +118 -0
  63. package/dist-client/modeller/figure-view.js +78 -0
  64. package/dist-client/modeller/figure-view.js.map +1 -0
  65. package/dist-client/modeller/side-panel-styles.d.ts +23 -0
  66. package/dist-client/modeller/side-panel-styles.js +326 -0
  67. package/dist-client/modeller/side-panel-styles.js.map +1 -0
  68. package/dist-client/modeller/v3-canvas.d.ts +29 -37
  69. package/dist-client/modeller/v3-canvas.js +188 -260
  70. package/dist-client/modeller/v3-canvas.js.map +1 -1
  71. package/dist-client/modeller/v3-clips.d.ts +39 -0
  72. package/dist-client/modeller/v3-clips.js +58 -0
  73. package/dist-client/modeller/v3-clips.js.map +1 -0
  74. package/dist-client/modeller/v3-controls.d.ts +28 -0
  75. package/dist-client/modeller/v3-controls.js +28 -0
  76. package/dist-client/modeller/v3-controls.js.map +1 -0
  77. package/dist-client/modeller/v3-editor.d.ts +58 -1
  78. package/dist-client/modeller/v3-editor.js +555 -218
  79. package/dist-client/modeller/v3-editor.js.map +1 -1
  80. package/dist-client/modeller/v3-fields.d.ts +30 -0
  81. package/dist-client/modeller/v3-fields.js +46 -0
  82. package/dist-client/modeller/v3-fields.js.map +1 -0
  83. package/dist-client/modeller/v3-help.d.ts +18 -0
  84. package/dist-client/modeller/v3-help.js +37 -0
  85. package/dist-client/modeller/v3-help.js.map +1 -0
  86. package/dist-client/modeller/v3-keys.d.ts +14 -0
  87. package/dist-client/modeller/v3-keys.js +23 -0
  88. package/dist-client/modeller/v3-keys.js.map +1 -0
  89. package/dist-client/modeller/v3-links.d.ts +30 -1
  90. package/dist-client/modeller/v3-links.js +94 -8
  91. package/dist-client/modeller/v3-links.js.map +1 -1
  92. package/dist-client/modeller/v3-live.d.ts +31 -0
  93. package/dist-client/modeller/v3-live.js +58 -0
  94. package/dist-client/modeller/v3-live.js.map +1 -0
  95. package/dist-client/modeller/v3-maturity.d.ts +120 -0
  96. package/dist-client/modeller/v3-maturity.js +317 -0
  97. package/dist-client/modeller/v3-maturity.js.map +1 -0
  98. package/dist-client/modeller/v3-palette.d.ts +7 -0
  99. package/dist-client/modeller/v3-palette.js +42 -0
  100. package/dist-client/modeller/v3-palette.js.map +1 -0
  101. package/dist-client/modeller/v3-preview-model.d.ts +32 -0
  102. package/dist-client/modeller/v3-preview-model.js +48 -0
  103. package/dist-client/modeller/v3-preview-model.js.map +1 -0
  104. package/dist-client/modeller/v3-stage-model.d.ts +31 -0
  105. package/dist-client/modeller/v3-stage-model.js +121 -0
  106. package/dist-client/modeller/v3-stage-model.js.map +1 -0
  107. package/dist-client/pages/figure-list-page.js +4 -4
  108. package/dist-client/pages/figure-list-page.js.map +1 -1
  109. package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
  110. package/dist-client/pages/figure-v3-modeller-page.js +64 -136
  111. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  112. package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
  113. package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -1
  114. package/dist-client/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +6 -6
  116. package/test/v3-clips.test.ts +70 -0
  117. package/test/v3-controls.test.ts +68 -0
  118. package/test/v3-fields.test.ts +62 -0
  119. package/test/v3-gizmo.test.ts +3 -5
  120. package/test/v3-keys.test.ts +37 -0
  121. package/test/v3-layout.test.ts +78 -0
  122. package/test/v3-links.test.ts +77 -6
  123. package/test/v3-live.test.ts +99 -0
  124. package/test/v3-material.test.ts +6 -0
  125. package/test/v3-maturity.test.ts +268 -0
  126. package/test/v3-parts-wiring.test.ts +3 -1
  127. package/test/v3-preview.test.ts +42 -0
  128. package/test/v3-properties.test.ts +4 -2
  129. package/test/v3-scene-canvas.test.ts +85 -0
  130. package/test/v3-stage-model.test.ts +25 -0
  131. package/test/v3-theme.test.ts +16 -25
  132. package/test/v3-verdict.test.ts +12 -6
  133. package/test/v3-view.test.ts +20 -0
  134. package/translations/en.json +84 -5
  135. package/translations/ja.json +86 -7
  136. package/translations/ko.json +84 -5
  137. package/translations/ms.json +86 -7
  138. package/translations/zh.json +86 -7
@@ -0,0 +1,326 @@
1
+ import { css, html, LitElement, nothing, unsafeCSS } from 'lit'
2
+ import { customElement, property, query, state } from 'lit/decorators.js'
3
+
4
+ import { i18next, localize } from '@operato/i18n'
5
+ import { compileV3Asset } from '@hatiolab/figure-model'
6
+ import type { V3Asset } from '@hatiolab/figure-model'
7
+ import { create, registerFigureV3, unregisterFigureV3 } from '@hatiolab/things-scene'
8
+
9
+ import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from './figure-view.js'
10
+ import { dialShape } from './angle-dial.js'
11
+ import { authoredSizeOf, PREVIEW_FACTOR, PREVIEW_WAYS, previewInstances } from './v3-preview-model.js'
12
+
13
+ /**
14
+ * The figure placed on a board -- V2's preview, for a V3 figure.
15
+ *
16
+ * The editing canvas shows the figure at the size it was made. Half of what a figure is lies in how it answers a
17
+ * different size, so here the figure being authored is registered as a board registers it and placed as
18
+ * `FigureInstance`s: the size it was made, and each of the seven ways of growing one, two or three axes twice as
19
+ * large, side by side at one scale, four to a row (V2's eight). The board draws them as it will on a real board --
20
+ * placement, lights, environment, the same V3 renderer.
21
+ *
22
+ * The controls are V2's too: each value that poses the figure with a slider (and a dial for an angle), each clip's
23
+ * speed, and 「이 자세를 기본으로」 to keep the pose as the one it stands in. Nothing here is edited; the canvas edits.
24
+ */
25
+
26
+ interface SceneHandle {
27
+ dispose?: () => void
28
+ resize?: () => void
29
+ fit?: (type?: string) => void
30
+ root?: {
31
+ set?: (patch: Record<string, unknown>) => void
32
+ fitCameraToModel?: () => void
33
+ components?: { set?: (property: string, value: unknown) => void }[]
34
+ }
35
+ }
36
+
37
+ @customElement('figure-v3-preview')
38
+ export class FigureV3Preview extends localize(i18next)(LitElement) {
39
+ static styles = css`
40
+ :host {
41
+ position: relative;
42
+ display: flex;
43
+ min-width: 0;
44
+ min-height: 0;
45
+ overflow: hidden;
46
+ background-color: ${unsafeCSS(WORKBENCH)};
47
+ font-size: 0.76rem;
48
+ }
49
+ div[stage] {
50
+ position: absolute;
51
+ inset: 0;
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+ /* Which is which: side by side means nothing without a legend. */
56
+ div[legend] {
57
+ position: absolute;
58
+ top: 8px;
59
+ left: 8px;
60
+ display: flex;
61
+ gap: 6px;
62
+ padding: 4px 8px;
63
+ color: rgba(255, 255, 255, 0.9);
64
+ background-color: rgba(28, 28, 30, 0.6);
65
+ border-radius: 6px;
66
+ font-size: 0.72rem;
67
+ }
68
+ div[clips] {
69
+ position: absolute;
70
+ right: 8px;
71
+ bottom: 8px;
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 6px;
75
+ padding: 8px 10px;
76
+ color: rgba(255, 255, 255, 0.92);
77
+ background-color: rgba(28, 28, 30, 0.72);
78
+ border-radius: 8px;
79
+ font-size: 0.72rem;
80
+ }
81
+ div[clips] label {
82
+ display: grid;
83
+ grid-template-columns: auto 20px 96px 34px;
84
+ align-items: center;
85
+ gap: 8px;
86
+ }
87
+ div[clips] svg[dial] {
88
+ width: 20px;
89
+ height: 20px;
90
+ }
91
+ div[clips] svg[dial] path {
92
+ fill: rgba(255, 255, 255, 0.22);
93
+ }
94
+ div[clips] svg[dial] line {
95
+ stroke: var(--md-sys-color-primary, #7cc6ff);
96
+ stroke-width: 2;
97
+ stroke-linecap: round;
98
+ }
99
+ div[clips] svg[dial] circle {
100
+ fill: none;
101
+ stroke: rgba(255, 255, 255, 0.35);
102
+ }
103
+ div[clips] button[pose] {
104
+ align-self: stretch;
105
+ padding: 4px 8px;
106
+ font: inherit;
107
+ font-size: 0.7rem;
108
+ color: rgba(255, 255, 255, 0.92);
109
+ background-color: rgba(255, 255, 255, 0.14);
110
+ border: 1px solid rgba(255, 255, 255, 0.22);
111
+ border-radius: 5px;
112
+ cursor: pointer;
113
+ }
114
+ div[clips] button[pose]:hover {
115
+ background-color: rgba(255, 255, 255, 0.24);
116
+ }
117
+ div[clips] span[drive] {
118
+ opacity: 0.6;
119
+ font-size: 0.66rem;
120
+ }
121
+ div[clips] input {
122
+ width: 96px;
123
+ accent-color: var(--md-sys-color-primary);
124
+ }
125
+ div[clips] output {
126
+ font-variant-numeric: tabular-nums;
127
+ text-align: right;
128
+ opacity: 0.85;
129
+ }
130
+ div[cannot] {
131
+ position: absolute;
132
+ inset: 0;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ padding: 12px;
137
+ text-align: center;
138
+ line-height: 1.6;
139
+ color: #f4f4f4;
140
+ background-color: ${unsafeCSS(WORKBENCH)};
141
+ }
142
+ `
143
+
144
+ /** The figure being authored, as the editor holds it now. */
145
+ @property({ attribute: false }) asset?: V3Asset
146
+ /** Which figure: the type it is registered under here is this one's own, never a real type a board uses. */
147
+ @property() context = ''
148
+ @property({ attribute: false }) view: ViewSettings = DEFAULT_VIEW
149
+
150
+ @query('div[stage]') private stage!: HTMLDivElement
151
+ @state() private cannot = ''
152
+ /** Clip speeds and control values given here -- the preview's, not the figure's. */
153
+ @state() private driving: Record<string, number> = {}
154
+ @state() private giving: Record<string, number> = {}
155
+
156
+ private scene?: SceneHandle
157
+ private standing?: string
158
+ private registered?: string
159
+ private watcher?: ResizeObserver
160
+
161
+ render() {
162
+ const asset = this.asset
163
+ const controls = asset ? asset.document.model.inputs.filter(i => i.role === 'state') : []
164
+ const clips = asset ? [...new Set((asset.drivers ?? []).map(d => d.clip))] : []
165
+ return html`<div stage></div>
166
+ ${this.cannot
167
+ ? html`<div cannot>${this.cannot}</div>`
168
+ : html`<div legend>${PREVIEW_WAYS.map(way => html`<span>${way.axes.length ? way.name : i18next.t('figure.label.preview-base')}</span>`)}</div>
169
+ ${controls.length + clips.length ? this.renderDrivers(asset!, controls, clips) : nothing}`}`
170
+ }
171
+
172
+ private renderDrivers(asset: V3Asset, controls: V3Asset['document']['model']['inputs'], clips: string[]) {
173
+ const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
174
+ return html`<div clips>
175
+ ${changed.length
176
+ ? html`<button pose title=${i18next.t('figure.text.pose-as-default-explained')} @click=${() => this.poseAsDefault()}>
177
+ ${i18next.t('figure.button.pose-as-default')}
178
+ </button>`
179
+ : nothing}
180
+ ${controls.map(control => {
181
+ const value = this.giving[control.id] ?? asset.stateDefaults[control.id] ?? control.min
182
+ const step = Math.max((control.max - control.min) / 100, 0.01)
183
+ const label = asset.stateInputs?.[control.id]?.label ?? control.id
184
+ return html`<label>
185
+ <span>${label} <span drive>${control.unit}</span></span>
186
+ ${this.renderDial(control, value)}
187
+ <input
188
+ type="range"
189
+ min=${control.min}
190
+ max=${control.max}
191
+ step=${step}
192
+ .value=${String(value)}
193
+ @input=${(e: Event) => this.give(control.id, Number((e.target as HTMLInputElement).value))}
194
+ />
195
+ <output>${Math.round(value)}</output>
196
+ </label>`
197
+ })}
198
+ ${clips.map(clip => {
199
+ const value = this.driving[clip] ?? 1
200
+ return html`<label>
201
+ <span>${clip} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
202
+ <span></span>
203
+ <input type="range" min="0" max="3" step="0.05" .value=${String(value)} @input=${(e: Event) => this.drive(clip, Number((e.target as HTMLInputElement).value))} />
204
+ <output>${value.toFixed(2)}</output>
205
+ </label>`
206
+ })}
207
+ </div>`
208
+ }
209
+
210
+ private renderDial(control: { unit: string; min: number; max: number }, value: number) {
211
+ if (control.unit !== 'deg') return html`<span></span>`
212
+ const dial = dialShape(control.min, control.max, value)
213
+ const span = dial.radius * 2 + 2
214
+ return html`<svg dial viewBox="0 0 ${span} ${span}" aria-hidden="true">
215
+ <path d=${dial.sector}></path>
216
+ <circle cx=${dial.centre.x} cy=${dial.centre.y} r=${dial.radius}></circle>
217
+ <line x1=${dial.centre.x} y1=${dial.centre.y} x2=${dial.needle.x} y2=${dial.needle.y}></line>
218
+ </svg>`
219
+ }
220
+
221
+ private drive(clip: string, value: number) {
222
+ this.driving = { ...this.driving, [clip]: value }
223
+ this.tellInstances('clips', this.driving)
224
+ }
225
+
226
+ private give(id: string, value: number) {
227
+ this.giving = { ...this.giving, [id]: value }
228
+ this.tellInstances('parameters', this.giving)
229
+ }
230
+
231
+ private poseAsDefault() {
232
+ this.dispatchEvent(new CustomEvent('v3-part-edit', { detail: { edit: { kind: 'set-state-defaults', values: { ...this.giving } } }, bubbles: true, composed: true }))
233
+ this.giving = {}
234
+ }
235
+
236
+ private tellInstances(key: 'clips' | 'parameters', values: Record<string, number>) {
237
+ for (const component of this.scene?.root?.components ?? []) component.set?.(key, { ...values })
238
+ }
239
+
240
+ willUpdate(changed: Map<string, unknown>) {
241
+ if (!this.hasUpdated) return
242
+ if (changed.has('asset') || changed.has('context')) {
243
+ this.standUp()
244
+ return
245
+ }
246
+ if (!changed.has('view')) return
247
+ const before = changed.get('view') as ViewSettings | undefined
248
+ const now = this.view ?? DEFAULT_VIEW
249
+ if (before?.placement !== now.placement || before?.ceilingHeight !== now.ceilingHeight) this.standUp()
250
+ else this.scene?.root?.set?.(sceneLook(now))
251
+ }
252
+
253
+ firstUpdated() {
254
+ this.standUp()
255
+ this.watcher = new ResizeObserver(() => this.scene?.resize?.())
256
+ this.watcher.observe(this)
257
+ }
258
+
259
+ disconnectedCallback() {
260
+ super.disconnectedCallback()
261
+ this.watcher?.disconnect()
262
+ this.watcher = undefined
263
+ this.teardown()
264
+ }
265
+
266
+ private standUp() {
267
+ const asset = this.asset
268
+ const view = this.view ?? DEFAULT_VIEW
269
+ if (!asset || !asset.appearance.length) {
270
+ this.cannot = i18next.t('figure.text.preview-needs-a-part')
271
+ this.teardown()
272
+ return
273
+ }
274
+ const signature = JSON.stringify([asset, view.placement, view.ceilingHeight])
275
+ if (this.scene && this.standing === signature) return
276
+ try {
277
+ compileV3Asset(asset)
278
+ } catch {
279
+ this.cannot = i18next.t('figure.text.preview-blocked-by-errors')
280
+ this.teardown()
281
+ return
282
+ }
283
+ this.teardown()
284
+ this.driving = {}
285
+ try {
286
+ /* Registered as a board registers it, under a type of this preview's own: a real type's drawings stay as they are. */
287
+ const type = `figure-v3-preview-${this.context || 'new'}`
288
+ registerFigureV3(type, asset)
289
+ this.registered = type
290
+ const size = authoredSizeOf(asset)
291
+ const { components, board } = previewInstances(type, size)
292
+ this.scene = create({
293
+ target: this.stage,
294
+ model: {
295
+ type: 'model-layer',
296
+ width: board.width,
297
+ height: board.height,
298
+ depth: previewBoardDepth({ placement: asset.occupancy?.placement, base: { y: size.y } }, view.ceilingHeight, PREVIEW_FACTOR),
299
+ threed: true,
300
+ sky: 'color',
301
+ ...sceneLook(view),
302
+ placement: view.placement,
303
+ heights: { floor: 0, ceiling: view.ceilingHeight },
304
+ pixelRatio: Math.min(3, (window.devicePixelRatio || 1) * 2),
305
+ components
306
+ },
307
+ mode: 0
308
+ }) as SceneHandle
309
+ this.standing = signature
310
+ this.scene.resize?.()
311
+ this.scene.fit?.('ratio')
312
+ this.scene.root?.fitCameraToModel?.()
313
+ this.cannot = ''
314
+ } catch (error) {
315
+ this.cannot = (error as Error).message
316
+ }
317
+ }
318
+
319
+ private teardown() {
320
+ this.scene?.dispose?.()
321
+ this.scene = undefined
322
+ this.standing = undefined
323
+ if (this.registered) unregisterFigureV3(this.registered)
324
+ this.registered = undefined
325
+ }
326
+ }
@@ -9,6 +9,7 @@ import type { V3AuthoringAction, V3Face } from '@hatiolab/figure-model'
9
9
 
10
10
  import { occupancyFromParts } from './v3-occupancy.js'
11
11
  import { tagsOf, v3FigureFacts, type Placement, type V3FigureFacts } from './v3-figure-facts.js'
12
+ import { SidePanelStyles } from './side-panel-styles.js'
12
13
 
13
14
  const PLACEMENTS: Placement[] = ['floor', 'ceiling', 'center']
14
15
  const FACES = Object.keys(V3_FACES) as V3Face[]
@@ -31,71 +32,30 @@ const FACES = Object.keys(V3_FACES) as V3Face[]
31
32
  */
32
33
  @customElement('figure-v3-properties')
33
34
  export class FigureV3Properties extends localize(i18next)(LitElement) {
34
- static styles = css`
35
+ static styles = [SidePanelStyles, css`
35
36
  :host {
36
- display: flex;
37
- flex-direction: column;
38
- gap: 12px;
39
- padding: 12px;
37
+ display: block;
38
+ /* It stands inside the properties tab's panel, which gives the margin; a second one indented it past the type. */
39
+ padding: 0;
40
40
  overflow: auto;
41
- font-size: 0.76rem;
42
- color: var(--md-sys-color-on-surface);
43
- }
44
- div[field] {
45
- display: grid;
46
- gap: 4px;
47
- }
48
- div[field] > label {
49
- font-weight: 600;
50
41
  }
51
42
  div[row] {
52
43
  display: flex;
53
44
  gap: 6px;
54
45
  align-items: center;
55
46
  }
56
- input,
57
- select,
58
- button {
59
- font: inherit;
60
- min-width: 0;
61
- }
62
- input,
63
- select {
64
- padding: 5px 8px;
65
- border: 1px solid var(--md-sys-color-outline-variant);
66
- border-radius: 4px;
67
- background: var(--md-sys-color-surface);
68
- color: var(--md-sys-color-on-surface);
69
- }
70
47
  div[row] > select {
71
48
  flex: 1;
72
49
  }
73
- button {
74
- display: inline-flex;
75
- align-items: center;
76
- gap: 4px;
77
- padding: 4px 10px;
78
- border: none;
79
- border-radius: 999em;
80
- cursor: pointer;
81
- color: var(--md-sys-color-on-secondary-container);
82
- background: var(--md-sys-color-secondary-container);
83
- }
84
- button:disabled,
85
- input:disabled,
86
- select:disabled {
87
- opacity: 0.5;
88
- cursor: default;
89
- }
90
- button md-icon {
91
- font-size: 16px;
92
- width: 16px;
93
- height: 16px;
50
+ div[row] > button[act] {
51
+ flex: 1;
52
+ width: auto;
53
+ margin-top: 0;
94
54
  }
95
- small {
96
- color: var(--md-sys-color-on-surface-variant);
55
+ div[row] > span {
56
+ flex: 1;
97
57
  }
98
- `
58
+ `]
99
59
 
100
60
  /** The source being edited. */
101
61
  @property({ type: String }) source?: string
@@ -119,8 +79,20 @@ export class FigureV3Properties extends localize(i18next)(LitElement) {
119
79
  render() {
120
80
  const facts = this.facts()
121
81
  if (!facts) return nothing
122
- return html`${this.renderName(facts)} ${this.renderPlacement(facts)} ${this.renderSpace(facts)} ${this.renderMounting(facts)}
123
- ${this.renderCatalog()}`
82
+ /* The figure's type is the page's (it lives on the row); it is slotted into the first section, as V2 had it. */
83
+ return html`<section>
84
+ <h3>${i18next.t('figure.label.group-figure')}</h3>
85
+ <slot name="type"></slot>
86
+ ${this.renderName(facts)} ${this.renderPlacement(facts)}
87
+ </section>
88
+ <section>
89
+ <h3>${i18next.t('figure.label.group-standing')}</h3>
90
+ ${this.renderSpace(facts)} ${this.renderMounting(facts)}
91
+ </section>
92
+ <section>
93
+ <h3>${i18next.t('figure.label.group-catalog')}</h3>
94
+ ${this.renderCatalog()}
95
+ </section>`
124
96
  }
125
97
 
126
98
  private renderName(facts: V3FigureFacts) {
@@ -172,7 +144,7 @@ export class FigureV3Properties extends localize(i18next)(LitElement) {
172
144
  <label>${i18next.t('figure.label.declared-space')}</label>
173
145
  <div row>
174
146
  <span>${i18next.t(facts.declared ? 'figure.text.declared-space-declared' : 'figure.text.declared-space-none')}</span>
175
- <button fit ?disabled=${!facts.parts.length} @click=${() => this.declare(facts.placement ?? 'floor')}>
147
+ <button act ?disabled=${!facts.parts.length} @click=${() => this.declare(facts.placement ?? 'floor')}>
176
148
  <md-icon>fit_screen</md-icon>${i18next.t('figure.button.fit-base-to-parts')}
177
149
  </button>
178
150
  </div>
@@ -196,10 +168,10 @@ export class FigureV3Properties extends localize(i18next)(LitElement) {
196
168
  </select>
197
169
  </div>
198
170
  <div row>
199
- <button ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>
171
+ <button act ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>
200
172
  ${i18next.t('figure.button.set-mounting-face')}
201
173
  </button>
202
- <button ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
174
+ <button act ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
203
175
  ${i18next.t('figure.button.clear-mounting-face')}
204
176
  </button>
205
177
  </div>