@things-factory/figure-ui 10.1.93 → 10.1.95

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.
@@ -312,13 +312,18 @@ export class FigureEditor extends LitElement {
312
312
  z-index: 1;
313
313
  display: grid;
314
314
  gap: 4px;
315
- padding: 8px 10px;
315
+ padding: 10px 12px;
316
316
  background: var(--md-sys-color-surface-container-lowest);
317
317
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
318
318
  }
319
+ /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */
319
320
  div[subject-summary] {
320
321
  display: grid;
321
- gap: 1px;
322
+ gap: 2px;
323
+ }
324
+ div[subject-summary] b {
325
+ font-size: var(--ui-t-body, 13px);
326
+ font-weight: 600;
322
327
  }
323
328
  div[subject-summary] b span {
324
329
  font-weight: 400;
@@ -2122,6 +2127,7 @@ export class FigureEditor extends LitElement {
2122
2127
  ? html`<section>
2123
2128
  <h3>${i18next.t(vectorTitle)}</h3>
2124
2129
  ${groups.vectors.map(g => this.renderVector(g, unavailable, held))}
2130
+ ${this.renderHeldAxes(groups.vectors)}
2125
2131
  ${tools ?? nothing}
2126
2132
  </section>`
2127
2133
  : nothing}
@@ -2176,7 +2182,22 @@ export class FigureEditor extends LitElement {
2176
2182
  )}
2177
2183
  </div>
2178
2184
  </div>
2179
- ${ruled.length ? html`<p hint held-axes>${i18next.t('figure.text.axes-held-by-rule', { group: i18next.t('figure.label.group-' + group.name), axes: ruled.map(a => a.toUpperCase()).join(' · ') })}</p>` : nothing}`
2185
+ `
2186
+ }
2187
+ /**
2188
+ * Which axes a rule sets, said once under the rows (position and rotation X · Y · Z set by a rule), not a line under each row
2189
+ * saying the same (user 2026-10-01, the panel checked for balance). The cells themselves are dashed and read-only.
2190
+ */
2191
+ private renderHeldAxes(vectors: FieldGroup[]) {
2192
+ const byAxes = new Map<string, string[]>()
2193
+ for (const g of vectors) {
2194
+ if (g.kind !== 'vector') continue
2195
+ const ruled = (['x', 'y', 'z'] as const).filter(axis => !g.cells[axis])
2196
+ if (!ruled.length) continue
2197
+ const axes = ruled.map(a => a.toUpperCase()).join(' · ')
2198
+ byAxes.set(axes, [...(byAxes.get(axes) ?? []), i18next.t('figure.label.group-' + g.name)])
2199
+ }
2200
+ return [...byAxes].map(([axes, groups]) => html`<p hint held-axes>${i18next.t('figure.text.axes-held-by-rule', { group: groups.join(' · '), axes })}</p>`)
2180
2201
  }
2181
2202
  /**
2182
2203
  * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
@@ -2191,7 +2212,7 @@ export class FigureEditor extends LitElement {
2191
2212
  f => html`<span>${fieldWordsOf(f.id, key => i18next.t(key))} (${f.unit})</span><input aria-label=${f.id} type="number" .value=${String(f.value)} disabled />`
2192
2213
  )}
2193
2214
  </div>
2194
- <button act room-refit ?disabled=${unavailable} @click=${() => this.askRoomRefit()}>${i18next.t('figure.button.room-refit')}</button>
2215
+ <button act room-refit ?disabled=${unavailable} @click=${() => this.askRoomRefit()}><md-icon>fit_screen</md-icon>${i18next.t('figure.button.room-refit')}</button>
2195
2216
  </section>`
2196
2217
  }
2197
2218
  /** The room as declared and as proposed, and what lies outside each as measured, before the author confirms. */
@@ -22,6 +22,7 @@ const SEEN: Array<[FigureFace, string]> = [
22
22
  ['right', 'M160 30 L200 10 L200 80 L160 100 Z']
23
23
  ]
24
24
  /** The faces the drawing cannot show on the box, as tags beside it where they are. */
25
+ /* The box is drawn a little left of its picture's middle, and the viewBox shifts it back to the panel's centre. */
25
26
  const HIDDEN: Array<[FigureFace, number, number]> = [
26
27
  ['bottom', 30, 114],
27
28
  ['left', 24, 62],
@@ -64,12 +65,17 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
64
65
  grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
65
66
  gap: 6px;
66
67
  }
68
+ div[modes] svg {
69
+ width: 64px;
70
+ height: 44px;
71
+ }
67
72
  div[modes] button {
68
73
  display: grid;
69
74
  justify-items: center;
75
+ align-content: center;
76
+ gap: 2px;
70
77
  padding: 2px 4px 4px;
71
78
  font: inherit;
72
- font-size: 0.72rem;
73
79
  color: var(--md-sys-color-on-surface);
74
80
  background: none;
75
81
  border: 1px solid var(--md-sys-color-outline-variant);
@@ -88,7 +94,6 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
88
94
  position: absolute;
89
95
  width: 72px;
90
96
  font: inherit;
91
- font-size: 0.75rem;
92
97
  }
93
98
  small[warn] {
94
99
  color: var(--md-sys-color-tertiary);
@@ -135,11 +140,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
135
140
 
136
141
  /** The target box, its faces pressed; or the plane. */
137
142
  private renderTarget() {
138
- if (!this.target)
139
- return svg`<svg viewBox="0 0 ${PANEL_W} 90" role="img" aria-label=${this.planeWord}>
140
- <path d="M30 80 L110 30 L270 30 L190 80 Z" style="fill:var(--md-sys-color-primary);fill-opacity:.35;stroke:var(--md-sys-color-primary);stroke-width:1.5" />
141
- <text x="150" y="62" text-anchor="middle" font-size="12" style="fill:var(--md-sys-color-on-primary-container)">${this.planeWord}</text>
142
- </svg>`
143
+ /* The plane is one face, named in the list and on the face drawn below: nothing to pick, so no picture of it. */
144
+ if (!this.target) return nothing
143
145
  const has = (f: string) => this.faces.includes(f)
144
146
  /* A press is a whole change, as a drag let go is: the form applies a fastening the part already has. */
145
147
  const pick = (f: string) => () => {
@@ -150,7 +152,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
150
152
  const seen = SEEN.map(([f, d]) => {
151
153
  const on = this.face === f
152
154
  return svg`<path face=${f} d=${d} @click=${pick(f)}
153
- style=${`cursor:${has(f) ? 'pointer' : 'default'};fill:${on ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-primary-container)'};fill-opacity:${on ? 0.55 : has(f) ? 1 : 0.35};stroke:var(--md-sys-color-primary);stroke-width:1.5`}>
155
+ style=${`cursor:${has(f) ? 'pointer' : 'default'};fill:${on ? 'var(--md-sys-color-primary-container)' : 'var(--md-sys-color-surface-container)'};fill-opacity:${has(f) ? 1 : 0.4};stroke:${on ? 'var(--md-sys-color-primary)' : 'var(--md-sys-color-outline)'};stroke-width:${on ? 2 : 1}`}>
154
156
  <title>${i18next.t('figure.label.face-' + f)}</title></path>`
155
157
  })
156
158
  const hidden = HIDDEN.map(([f, x, y]) => {
@@ -163,9 +165,9 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
163
165
  })
164
166
  /* A shape's faces beyond its six outer ones -- a door frame's opening -- are tags of their own under the box. */
165
167
  const others = this.faces.filter(f => !SEEN.some(([s]) => s === f) && !HIDDEN.some(([h]) => h === f))
166
- return html`${svg`<svg viewBox="0 0 ${PANEL_W} 126" role="img" aria-label=${this.target}>
168
+ return html`${svg`<svg viewBox="-20 0 ${PANEL_W} 126" role="img" aria-label=${this.target}>
167
169
  ${seen}
168
- <text x="110" y="70" text-anchor="middle" font-size="12" pointer-events="none" style="fill:var(--md-sys-color-on-primary-container)">${this.target}</text>
170
+ <text x="110" y="70" text-anchor="middle" font-size="12" pointer-events="none" style="fill:var(--md-sys-color-on-surface)">${this.target}</text>
169
171
  ${hidden}
170
172
  </svg>`}
171
173
  ${others.length
@@ -177,17 +179,19 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
177
179
 
178
180
  /** How: three side views; the chosen one's gap is dragged. */
179
181
  private renderModes() {
182
+ /* One way only (the plane is set on): the sentence says it; a single choice is not offered as a button. */
183
+ if (this.modes.length < 2) return nothing
180
184
  const icon = (m: FastenMode, on: boolean) => {
181
185
  const g = on && m !== 'centre' ? Math.min(this.gap / 4, 10) : 0
182
186
  const base = svg`<rect x="6" y="30" width="52" height="14" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary)" />`
183
187
  const gapDrag = on && !this.locked ? 'ns-resize' : 'pointer'
184
188
  if (m === 'meet')
185
- return svg`<svg width="64" height="48">${base}<rect gap x="22" y=${18 - g} width="20" height="12" style=${`fill:var(--md-sys-color-surface);stroke:currentColor;cursor:${gapDrag}`} />
189
+ return svg`<svg viewBox="0 0 64 48">${base}<rect gap x="22" y=${18 - g} width="20" height="12" style=${`fill:var(--md-sys-color-surface);stroke:currentColor;cursor:${gapDrag}`} />
186
190
  ${g ? svg`<text x="46" y=${26 - g / 2} font-size="10" style="fill:currentColor">${this.gap}</text>` : nothing}</svg>`
187
191
  if (m === 'flush')
188
- return svg`<svg width="64" height="48">${base}<rect gap x="22" y=${30 - g} width="20" height="10" style=${`fill:var(--md-sys-color-surface);stroke:currentColor;cursor:${gapDrag}`} />
192
+ return svg`<svg viewBox="0 0 64 48">${base}<rect gap x="22" y=${30 - g} width="20" height="10" style=${`fill:var(--md-sys-color-surface);stroke:currentColor;cursor:${gapDrag}`} />
189
193
  <line x1="4" y1="30" x2="60" y2="30" style="stroke:currentColor;stroke-dasharray:2 2" /></svg>`
190
- return svg`<svg width="64" height="48">${base}<circle cx="32" cy="30" r="11" style="fill:var(--md-sys-color-surface);fill-opacity:.85;stroke:currentColor" /><circle cx="32" cy="30" r="2" style="fill:currentColor" /></svg>`
194
+ return svg`<svg viewBox="0 0 64 48">${base}<circle cx="32" cy="30" r="11" style="fill:var(--md-sys-color-surface);fill-opacity:.85;stroke:currentColor" /><circle cx="32" cy="30" r="2" style="fill:currentColor" /></svg>`
191
195
  }
192
196
  return html`<div modes>
193
197
  ${this.modes.map(m => {
@@ -300,8 +304,8 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
300
304
  @pointermove=${(ev: PointerEvent) => this.drag(ev, view, cx, cy)}
301
305
  @pointerup=${() => this.drop()} @pointercancel=${() => this.drop()}>
302
306
  ${faceSized
303
- ? svg`<rect x=${fx} y=${fy} width=${fw} height=${fh} rx="2" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:1.5" />`
304
- : svg`<rect x="4" y="4" width=${W - 8} height=${H - 8} rx="6" style="fill:var(--md-sys-color-primary-container);fill-opacity:.5" />`}
307
+ ? svg`<rect x=${fx} y=${fy} width=${fw} height=${fh} rx="4" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />`
308
+ : svg`<rect x="1" y="1" width=${W - 2} height=${H - 2} rx="8" style="fill:var(--md-sys-color-primary-container);stroke:var(--md-sys-color-primary);stroke-width:2" />`}
305
309
  <text x=${faceSized ? fx + 4 : 10} y=${faceSized ? fy + fh - 6 : 18} font-size="11" pointer-events="none" style="fill:var(--md-sys-color-primary)">${faceName}${sizeWords}</text>
306
310
  ${guide(h)}${guide(v)}
307
311
  <g footprint style=${`cursor:${this.locked ? 'default' : 'grab'}`} @pointerdown=${(ev: PointerEvent) => this.grab(ev, at, caught)}>
@@ -119,17 +119,16 @@ export class FigureLinks extends localize(i18next)(LitElement) {
119
119
  div[resize-held] { display: grid; gap: 8px; margin: 12px 0; padding: 10px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
120
120
  div[resize-held] p { margin: 0; display: flex; align-items: center; gap: 6px; }
121
121
  p[fasten-mode] { margin: 0; font-weight: 600; }
122
- p[fasten-mode][editing] { color: var(--md-sys-color-primary, #477b9e); }
123
122
  section[fastening][spotlit] { outline: 2px solid var(--md-sys-color-primary, #477b9e); outline-offset: 4px; border-radius: 6px; transition: outline-color 0.3s; }
124
- details { margin-top: 14px; }
125
- summary { cursor: pointer; padding: 8px 0; }
123
+ details { margin-top: 8px; }
126
124
  /* What the command would do, said beside the result drawn on the canvas. */
127
- ul[preview-words] { margin: 10px 0; padding: 8px 12px 8px 26px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); display: grid; gap: 4px; }
125
+ ul[preview-words] { margin: 8px 0; padding: 8px 12px 8px 26px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); display: grid; gap: 4px; }
126
+ p[preview-words] { margin: 8px 0; padding: 8px 12px; border-radius: 6px; background: var(--md-sys-color-surface-container, #8881); }
128
127
  div[row-basis] { display: grid; gap: 4px; margin: 8px 0; }
129
128
  div[row-basis] label { display: flex; align-items: center; gap: 6px; }
130
129
  div[overhang] { display: grid; gap: 6px; margin: 8px 0; padding: 8px 12px; border-radius: 6px; border: 1px solid var(--md-sys-color-outline-variant, #8884); }
131
130
  div[overhang] p { margin: 0; }
132
- ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; font-size: 0.8rem; opacity: 0.85; }
131
+ ul[detach-words] { margin: 4px 0 10px; padding-left: 20px; display: grid; gap: 2px; }
133
132
  span[fasten-summary] { opacity: 0.8; }
134
133
  /* The difference a following measure keeps: under its choice, on the same line as its words. */
135
134
  label[inline-plus] {
@@ -443,11 +442,11 @@ export class FigureLinks extends localize(i18next)(LitElement) {
443
442
  return html`<div resize-held>
444
443
  <p><md-icon>link</md-icon> ${where}</p>
445
444
  <button act open-fastening @click=${() => this.openFastening(asset)}>
446
- ${i18next.t('figure.button.open-fastening')}
445
+ <md-icon>link</md-icon>${i18next.t('figure.button.open-fastening')}
447
446
  </button>
448
447
  ${joint
449
448
  ? html`<button act detach-held ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
450
- ${i18next.t('figure.button.detach-held')}
449
+ <md-icon>link_off</md-icon>${i18next.t('figure.button.detach-held')}
451
450
  </button>
452
451
  ${this.renderDetachWords()}`
453
452
  : nothing}
@@ -569,17 +568,12 @@ export class FigureLinks extends localize(i18next)(LitElement) {
569
568
  ...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
570
569
  ]
571
570
  /*
572
- Editing the fastening it has, or fastening it anew: said, with the relation named, so the form is not taken for a
573
- blank new one while it holds the current (designer 2026-09-27). Applied unchanged, the same fastening is written
574
- again, fields and all (resize-with-placement.test).
571
+ Fastening it anew is said, so the form is not taken for the one it has (designer 2026-09-27). The one it has is
572
+ said once, by the sentence over the drawing -- the panel's head says it too; a third and a fourth line saying the
573
+ same (「현재 관계 수정」, 「붙은 곳」) went (user 2026-10-01, the panel checked for balance).
575
574
  */
576
575
  const current = asset.joints?.find(j => j.body1.part === this.partId)
577
- const editing = current
578
- ? i18next.t('figure.text.fasten-editing', {
579
- face: i18next.t(`figure.label.face-${current.body1.face}`),
580
- target: 'plane' in current.body0 ? i18next.t('figure.label.mounting-plane') : `${current.body0.part} · ${i18next.t(`figure.label.face-${current.body0.face}`)}`
581
- })
582
- : i18next.t('figure.text.fasten-new')
576
+ const editing = i18next.t('figure.text.fasten-new')
583
577
  /* A blank attachment is not a form to decipher. Ask for the thing to attach to first. */
584
578
  if (!current && !this.choosingFastening && !f.target)
585
579
  return html`
@@ -589,7 +583,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
589
583
  </button>
590
584
  `
591
585
  return html`
592
- <p fasten-mode ?editing=${!!current}>${editing}</p>
586
+ ${current ? nothing : html`<p fasten-mode>${editing}</p>`}
593
587
  ${this.overhangNote.length ? html`<p hint overhang-note>${this.overhangNote.join(' ')}</p>` : nothing}
594
588
  ${this.overhangWords.length
595
589
  ? html`<div overhang role="status">
@@ -600,13 +594,12 @@ export class FigureLinks extends localize(i18next)(LitElement) {
600
594
  </div>`
601
595
  : nothing}
602
596
  ${held
603
- ? html`<div pair>
604
- <span>${i18next.t('figure.label.fastened-to')}: <b bound>${'plane' in held ? i18next.t('figure.label.mounting-plane') : held.part}</b></span>
597
+ ? html`<div fastened>
605
598
  <button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'detach', part: this.partId })}>
606
599
  <md-icon>link_off</md-icon>${i18next.t('figure.button.detach')}
607
600
  </button>
608
- </div>
609
- ${this.renderDetachWords()}`
601
+ ${this.renderDetachWords()}
602
+ </div>`
610
603
  : nothing}
611
604
  <div form>
612
605
  <span>${i18next.t('figure.label.target-part')}</span>
@@ -687,6 +680,8 @@ export class FigureLinks extends localize(i18next)(LitElement) {
687
680
  }
688
681
 
689
682
  private renderDetachWords() {
683
+ /* One thing to say is a line, not a list of one. */
684
+ if (this.detachWords.length === 1) return html`<p hint detach-words>${this.detachWords[0]}</p>`
690
685
  return this.detachWords.length ? html`<ul detach-words>${this.detachWords.map(line => html`<li>${line}</li>`)}</ul>` : nothing
691
686
  }
692
687
 
@@ -729,6 +724,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
729
724
  * (the editor's) shows it and takes it away.
730
725
  */
731
726
  private renderPreviewWords() {
727
+ if (this.previewWords.length === 1) return html`<p preview-words role="status">${this.previewWords[0]}</p>`
732
728
  return this.previewWords.length ? html`<ul preview-words role="status">${this.previewWords.map(line => html`<li>${line}</li>`)}</ul>` : nothing
733
729
  }
734
730
 
@@ -49,8 +49,16 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
49
49
  display: grid;
50
50
  gap: 4px;
51
51
  }
52
- div[field] > label {
53
- font-weight: 600;
52
+ /* A slider and its value on one line, the value at the end where the eye finishes. */
53
+ div[slider] {
54
+ display: grid;
55
+ grid-template-columns: minmax(0, 1fr) 3em;
56
+ align-items: center;
57
+ gap: 8px;
58
+ }
59
+ div[slider] output {
60
+ text-align: right;
61
+ font-variant-numeric: tabular-nums;
54
62
  }
55
63
  div[swatches] {
56
64
  display: flex;
@@ -118,16 +126,14 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
118
126
  fill: var(--md-sys-color-surface-container-lowest);
119
127
  }
120
128
  button[seg] b {
121
- font-size: 0.8rem;
129
+ font-weight: 600;
122
130
  }
123
131
  button[seg] small {
124
132
  margin: 0;
125
- font-size: 0.6rem;
126
133
  }
127
134
  details[free] summary {
128
135
  margin-top: 4px;
129
136
  cursor: pointer;
130
- font-size: 0.68rem;
131
137
  color: var(--md-sys-color-on-surface-variant);
132
138
  }
133
139
  div[presets] {
@@ -139,7 +145,6 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
139
145
  flex: 1;
140
146
  padding: 3px 0;
141
147
  font: inherit;
142
- font-size: 0.72rem;
143
148
  color: var(--md-sys-color-on-surface);
144
149
  background: none;
145
150
  border: 1px solid var(--md-sys-color-outline-variant);
@@ -184,8 +189,8 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
184
189
  : nothing}
185
190
  <small><b>${purposeOf(material.token)}</b> — ${i18next.t('figure.text.palette-purpose-explained')}</small>
186
191
  </div>
187
- <div field>
188
- <label>${i18next.t('figure.label.preset')}</label>
192
+ <div form>
193
+ <span>${i18next.t('figure.label.preset')}</span>
189
194
  <select
190
195
  ?disabled=${this.locked}
191
196
  @change=${(e: Event) => this.put({ preset: ((e.target as HTMLSelectElement).value || undefined) as FigureMaterial['preset'] })}
@@ -205,8 +210,9 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
205
210
  />
206
211
  ${i18next.t('figure.label.transparent')}
207
212
  </label>
208
- <div field>
209
- <label>${i18next.t('figure.label.emissive')}</label>
213
+ <div form>
214
+ <span>${i18next.t('figure.label.emissive')}</span>
215
+ <div slider>
210
216
  <input
211
217
  type="range"
212
218
  min="0"
@@ -217,7 +223,8 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
217
223
  @input=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value), true)}
218
224
  @change=${(e: Event) => this.glow(Number((e.target as HTMLInputElement).value))}
219
225
  />
220
- <small>${(this.glowing ?? glow?.intensity ?? 0).toFixed(2)}</small>
226
+ <output>${(this.glowing ?? glow?.intensity ?? 0).toFixed(2)}</output>
227
+ </div>
221
228
  </div>
222
229
  ${glow
223
230
  ? html`<div field>
@@ -177,10 +177,10 @@ export class FigureProperties extends localize(i18next)(LitElement) {
177
177
  </div>
178
178
  <div row>
179
179
  <button act ?disabled=${!facts.declared || !part} @click=${() => this.command({ kind: 'set-mounting-face', part: part!, face })}>
180
- ${i18next.t('figure.button.set-mounting-face')}
180
+ <md-icon>check</md-icon>${i18next.t('figure.button.set-mounting-face')}
181
181
  </button>
182
182
  <button act ?disabled=${!mounting} @click=${() => this.command({ kind: 'clear-mounting-face' })}>
183
- ${i18next.t('figure.button.clear-mounting-face')}
183
+ <md-icon>close</md-icon>${i18next.t('figure.button.clear-mounting-face')}
184
184
  </button>
185
185
  </div>
186
186
  <small>
@@ -1,3 +1,4 @@
1
+ import '@material/web/icon/icon.js'
1
2
  import { css, html, LitElement, nothing, type PropertyValues } from 'lit'
2
3
  import { customElement, property, state } from 'lit/decorators.js'
3
4
  import { live } from 'lit/directives/live.js'
@@ -105,7 +106,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
105
106
  ${body.shape.dimensions.jamb === undefined
106
107
  ? html`<div field>
107
108
  <button act ?disabled=${this.locked} @click=${() => this.dispatchEvent(new CustomEvent('section-editor-open', { bubbles: true, composed: true }))}>
108
- ${i18next.t('figure.button.section-editor-open')}
109
+ <md-icon>edit</md-icon>${i18next.t('figure.button.section-editor-open')}
109
110
  </button>
110
111
  ${e.section.kind === 'polygon'
111
112
  ? html`<small>${i18next.t(e.apex ? 'figure.text.section-points-apex' : 'figure.text.section-points', { points: String(e.section.points.length) })}</small>`
@@ -128,8 +129,9 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
128
129
  .map(k => d[k])
129
130
  const now = reaches.length ? sameOf(reaches) : 0
130
131
  const max = cornerLimitOf(body.shape)
131
- return html`<div field>
132
+ return html`<div form>
132
133
  <label for="corner">${i18next.t('figure.label.section-corner')}</label>
134
+ <div>
133
135
  ${this.number('corner', now, {
134
136
  min: 0,
135
137
  ask: value => {
@@ -140,6 +142,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
140
142
  placeholder: now === undefined ? i18next.t('figure.text.section-corner-differs') : ''
141
143
  })}
142
144
  ${this.refusals.corner ? nothing : max !== undefined ? html`<small>${i18next.t('figure.text.section-at-most', { max: String(mm(max)) })}</small>` : nothing}
145
+ </div>
143
146
  </div>`
144
147
  }
145
148
 
@@ -167,7 +170,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
167
170
  action: value => ({ kind: 'set-cavity', part: body.id, cavity: request(patch(value)) })
168
171
  })
169
172
  return html`<div field>
170
- <label>
173
+ <label toggle>
171
174
  <input
172
175
  type="checkbox"
173
176
  .checked=${live(!!cavity)}
@@ -191,8 +194,9 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
191
194
  </div>
192
195
  ${cavity ? html`<figure-cavity-drawing .body=${body} .perSide=${this.perSide} ?locked=${this.locked}></figure-cavity-drawing>` : nothing}
193
196
  ${cavity
194
- ? html`<div field>
197
+ ? html`<div form>
195
198
  <label>${i18next.t('figure.label.section-wall')}</label>
199
+ <div>
196
200
  ${this.perSide
197
201
  ? html`<div sides>
198
202
  ${[...sidesOf(axis)].sort((a, b) => a[0]!.localeCompare(b[0]!) || (a[1] === '-' ? -1 : 1)).map(
@@ -206,7 +210,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
206
210
  ${this.perSide || this.refusals.wall
207
211
  ? nothing
208
212
  : html`<small>${i18next.t('figure.text.section-wall-at-most', { max: String(mm(wallLimitOf(body.shape))) })}</small>`}
209
- <label>
213
+ <label toggle>
210
214
  <input
211
215
  type="checkbox"
212
216
  .checked=${live(this.perSide)}
@@ -215,6 +219,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
215
219
  />
216
220
  ${i18next.t('figure.label.section-wall-per-side')}
217
221
  </label>
222
+ </div>
218
223
  </div>
219
224
  ${(['start', 'end'] as const).map(end => this.renderEnd(body, axis, end, cavity[end], request, cavityField))}`
220
225
  : nothing}`
@@ -229,7 +234,7 @@ export class FigureSectionPanel extends localize(i18next)(LitElement) {
229
234
  cavityField: (key: string, now: number | undefined, patch: (value: number) => Partial<FigureCavityRequest>) => unknown
230
235
  ) {
231
236
  const closed = now !== 'open'
232
- return html`<div field>
237
+ return html`<div form>
233
238
  <label>${i18next.t('figure.label.face-' + END_FACE(axis, end))}</label>
234
239
  <div row>
235
240
  <select
@@ -10,7 +10,11 @@ import { css } from 'lit'
10
10
  *
11
11
  * The vocabulary:
12
12
  * section one titled group; the side is a stack of them
13
- * h3 a group's title -- small capitals
13
+ * h3 a group's title
14
+ *
15
+ * Three sizes and two weights (user 2026-10-01, the panel checked for balance: eight sizes, and titles smaller than
16
+ * what they titled): a title at the body token and semibold, everything else at the small token, and a hint at the
17
+ * micro one. Sections are 16 px above their title and 12 px below their last line.
14
18
  * div[inline-field] a short label and its one field on one line
15
19
  * div[field] a label above a full-width field
16
20
  * div[form] short labels beside their fields, the fields starting on one line
@@ -25,14 +29,14 @@ import { css } from 'lit'
25
29
  export const SidePanelStyles = css`
26
30
  :host {
27
31
  --figure-control-height: 30px;
28
- font-size: 0.76rem;
32
+ font-size: var(--ui-t-small, 11.5px);
29
33
  color: var(--md-sys-color-on-surface);
30
34
  }
31
35
  /* The side is a stack of sections, each a titled group. */
32
36
  section {
33
37
  display: grid;
34
- gap: 6px;
35
- padding: 8px 10px;
38
+ gap: 8px;
39
+ padding: 16px 12px 12px;
36
40
  border-bottom: 1px solid var(--md-sys-color-outline-variant);
37
41
  }
38
42
  section > h3 {
@@ -48,18 +52,21 @@ export const SidePanelStyles = css`
48
52
  align-items: center;
49
53
  gap: 4px;
50
54
  margin: 14px 0 6px;
51
- font-size: var(--ui-t-micro, 11px);
52
- font-weight: 700;
53
- letter-spacing: 0.04em;
54
- text-transform: uppercase;
55
- color: var(--md-sys-color-on-surface-variant);
55
+ font-size: var(--ui-t-body, 13px);
56
+ font-weight: 600;
57
+ color: var(--md-sys-color-on-surface);
56
58
  }
57
59
  h3:first-child {
58
60
  margin-top: 0;
59
61
  }
62
+ /* Two weights only: a bold word in a line is the title's weight, not the browser's heavier one. */
63
+ b,
64
+ strong {
65
+ font-weight: 600;
66
+ }
60
67
  label,
61
68
  div[form] > span {
62
- font-size: 0.68rem;
69
+ font-size: inherit;
63
70
  color: var(--md-sys-color-on-surface-variant);
64
71
  }
65
72
  div[field] {
@@ -90,7 +97,7 @@ export const SidePanelStyles = css`
90
97
  }
91
98
  /* A value read, not edited, beside its label: the field's size and colour, not the page's. */
92
99
  div[inline-field] > span[value] {
93
- font-size: 0.76rem;
100
+ font-size: inherit;
94
101
  color: var(--md-sys-color-on-surface);
95
102
  }
96
103
  label span[unit] {
@@ -108,10 +115,9 @@ export const SidePanelStyles = css`
108
115
  gap: 4px;
109
116
  }
110
117
  div[tools] button {
111
- height: 28px;
118
+ height: var(--figure-control-height);
112
119
  padding: 0 10px;
113
120
  font: inherit;
114
- font-size: 0.78rem;
115
121
  color: var(--md-sys-color-on-surface);
116
122
  background: var(--md-sys-color-surface-container-lowest);
117
123
  border: 1px solid var(--md-sys-color-outline-variant);
@@ -146,16 +152,45 @@ export const SidePanelStyles = css`
146
152
  box-sizing: border-box;
147
153
  padding: 4px 7px;
148
154
  font: inherit;
149
- font-size: 0.76rem;
150
155
  color: var(--md-sys-color-on-surface);
151
156
  background-color: var(--md-sys-color-surface-container-lowest);
152
157
  border: 1px solid var(--md-sys-color-outline-variant);
153
158
  border-radius: 6px;
154
159
  }
160
+ /* The browser's own box and dot, in the panel's colour. */
155
161
  input[type='checkbox'],
156
162
  input[type='radio'] {
157
163
  width: auto;
158
164
  min-height: 0;
165
+ margin: 0;
166
+ accent-color: var(--md-sys-color-primary);
167
+ }
168
+ /* A fold: the panel's chevron, not the browser's triangle. */
169
+ summary {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 6px;
173
+ padding: 6px 0;
174
+ list-style: none;
175
+ cursor: pointer;
176
+ color: var(--md-sys-color-on-surface-variant);
177
+ }
178
+ summary::-webkit-details-marker {
179
+ display: none;
180
+ }
181
+ summary::before {
182
+ content: '';
183
+ flex: none;
184
+ width: 6px;
185
+ height: 6px;
186
+ margin: 0 2px;
187
+ border-right: 1.5px solid currentColor;
188
+ border-bottom: 1.5px solid currentColor;
189
+ transform: rotate(-45deg);
190
+ transition: transform 0.15s;
191
+ }
192
+ details[open] > summary::before {
193
+ transform: rotate(45deg);
159
194
  }
160
195
  /* A thin track and a small thumb: a slider is a line to drag along, not a block (user, 2026-09-24). */
161
196
  input[type='range'] {
@@ -239,7 +274,7 @@ export const SidePanelStyles = css`
239
274
  }
240
275
  div[cell] span[axis] {
241
276
  flex: none;
242
- font-size: 0.64rem;
277
+ font-size: var(--ui-t-micro, 11px);
243
278
  letter-spacing: 0.02em;
244
279
  color: var(--md-sys-color-on-surface-variant);
245
280
  }
@@ -272,7 +307,6 @@ export const SidePanelStyles = css`
272
307
  display: flex;
273
308
  align-items: center;
274
309
  gap: 6px;
275
- font-size: 0.76rem;
276
310
  color: var(--md-sys-color-on-surface);
277
311
  }
278
312
  /* The 「?」 beside a title: no border, it says there is more rather than asking to be pressed. */
@@ -299,41 +333,42 @@ export const SidePanelStyles = css`
299
333
  small {
300
334
  display: block;
301
335
  margin: 2px 0 4px;
302
- font-size: 0.68rem;
336
+ font-size: var(--ui-t-micro, 11px);
303
337
  line-height: 1.5;
304
338
  color: var(--md-sys-color-on-surface-variant);
305
339
  }
306
- p[hint] {
307
- padding-left: 6px;
308
- border-left: 2px solid var(--md-sys-color-outline-variant);
309
- }
340
+ /*
341
+ * An action under its fields: as wide as its words, not a band across the panel -- four full-width grey bars read as
342
+ * four walls and none of them as the one to press (user 2026-10-01). Every one carries its icon.
343
+ */
310
344
  button[act] {
311
345
  display: inline-flex;
312
346
  align-items: center;
313
347
  justify-content: center;
314
- gap: 4px;
315
- width: 100%;
348
+ justify-self: start;
349
+ gap: 6px;
350
+ width: auto;
351
+ max-width: 100%;
316
352
  margin-top: 4px;
317
- padding: 3px 8px;
318
- min-height: 28px;
353
+ padding: 0 12px;
354
+ min-height: var(--figure-control-height);
319
355
  font: inherit;
320
- font-size: 0.72rem;
321
- color: var(--md-sys-color-on-surface);
322
- background-color: var(--md-sys-color-surface-container);
356
+ color: var(--md-sys-color-primary);
357
+ background-color: var(--md-sys-color-surface-container-lowest);
323
358
  border: 1px solid var(--md-sys-color-outline-variant);
324
359
  border-radius: 6px;
325
360
  cursor: pointer;
326
361
  }
327
362
  button[act]:hover:not(:disabled) {
328
- background-color: var(--md-sys-color-surface-container-high);
363
+ background-color: var(--md-sys-color-primary-container);
329
364
  }
330
365
  button[act]:disabled {
331
366
  opacity: 0.5;
332
367
  cursor: default;
333
368
  }
334
369
  button[act] md-icon {
335
- font-size: 15px;
336
- width: 15px;
337
- height: 15px;
370
+ font-size: 16px;
371
+ width: 16px;
372
+ height: 16px;
338
373
  }
339
374
  `
@@ -328,6 +328,11 @@ export declare class FigureEditor extends LitElement {
328
328
  */
329
329
  private withSizeRow;
330
330
  private renderVector;
331
+ /**
332
+ * Which axes a rule sets, said once under the rows (position and rotation X · Y · Z set by a rule), not a line under each row
333
+ * saying the same (user 2026-10-01, the panel checked for balance). The cells themselves are dashed and read-only.
334
+ */
335
+ private renderHeldAxes;
331
336
  /**
332
337
  * A part's fields only the declared room still reads (ADR-0097, designer 2026-09-27): shown as what they are and not
333
338
  * typed into -- they move nothing. Declaring the room again is offered; nothing is deleted or widened on the way.