@things-factory/figure-ui 10.1.194 → 10.1.195

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.
@@ -295,12 +295,13 @@ export const EditorStyles = css`
295
295
  }
296
296
  /*
297
297
  The view settings and the release history, opened from the bar over the right of the work: neither is about the
298
- part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16).
298
+ part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16). The fastening window stands in
299
+ the same place, over the side panel, so the canvas it previews on stays in sight (user 2026-10-19).
299
300
  */
300
301
  :host {
301
302
  position: relative;
302
303
  }
303
- div[bar-overlay-panel] {
304
+ div[side-overlay] {
304
305
  position: absolute;
305
306
  top: var(--figure-bar-height, 44px);
306
307
  right: 8px;
@@ -327,10 +328,16 @@ export const EditorStyles = css`
327
328
  div[overlay-head] button[quiet] {
328
329
  margin-left: 0;
329
330
  }
330
- div[overlay-body] {
331
+ [overlay-body] {
331
332
  min-height: 0;
332
333
  overflow: auto;
333
334
  }
335
+ /* The fastening window keeps to the panel's width: a popup's own sizing made it wider than the panel. */
336
+ div[fasten-panel] > figure-fasten-dialog {
337
+ box-sizing: border-box;
338
+ width: 100%;
339
+ min-width: 0;
340
+ }
334
341
  /* Which part the panel speaks of. */
335
342
  div[subject] {
336
343
  position: sticky;
@@ -139,12 +139,12 @@ export class FigureEditor extends LitElement {
139
139
  /** The view settings or the release history, open over the work from the bar. */
140
140
  @state() private overlay?: BarOverlay
141
141
  /**
142
- * The fastening window open for the selected part (user 2026-10-17), in the standard popup (2026-10-18): chosen there,
143
- * applied once. It closes with nothing written when the part, the tab or the middle changes under it.
142
+ * The fastening window open for the selected part (user 2026-10-17): chosen there, applied once. It stands over the
143
+ * side panel, not over the canvas, so what is chosen is seen in 3D as it is chosen (user 2026-10-19: the standard
144
+ * popup, always modal, covered the preview). It closes with nothing written when the part, the tab, the middle or a
145
+ * bar panel changes under it.
144
146
  */
145
147
  @state() private fastening = false
146
- private fastenPopup?: { close: () => void }
147
- private fastenWindow?: HTMLElement & { revision?: FigureRevision; partId: string; locked: boolean; previewWords: string[]; error: string }
148
148
  /** Why the window's 「적용」 was refused, said in it. */
149
149
  @state() private fastenError = ''
150
150
  /** The face of the part a row of the window's list is on, lit on the canvas while the pointer or the arrows are there. */
@@ -234,7 +234,7 @@ export class FigureEditor extends LitElement {
234
234
  aria-pressed=${String(this.gizmoMode === tool.mode)}
235
235
  ?disabled=${!tool.can}
236
236
  aria-label=${i18next.t('figure.button.gizmo-' + tool.mode)}
237
- title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + i18next.t(tool.mode === 'scale' && this.sizeDragUnsure(this.asset()!, this.selected) ? 'figure.text.gizmo-size-turned-fastening' : 'figure.text.gizmo-not-own', { part: partLabelOf(this.selected), extreme: this.keptExtremeWord() })}`}
237
+ title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + (tool.mode === 'scale' && this.sizeDragUnsure(this.asset()!, this.selected) ? this.sizeDragRefusalOf(this.selected) : i18next.t('figure.text.gizmo-not-own'))}`}
238
238
  @click=${() => this.useGizmo(tool.mode)}
239
239
  >
240
240
  <md-icon>${tool.icon}</md-icon>
@@ -292,7 +292,7 @@ export class FigureEditor extends LitElement {
292
292
  if (view && !view.geometry.some(g => ownerOf(g.id) === this.selected)) this.selected = ''
293
293
  if (_changed.has('selected')) this.gizmoMode = undefined
294
294
  /* The window is about the part chosen on the properties tab, being edited: it does not follow anywhere else. */
295
- if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit')) this.closeFastening()
295
+ if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit' || this.overlay)) this.closeFastening()
296
296
  /*
297
297
  * The section editor is of one part: when the selection moves off it by any way -- another part picked, the scene's
298
298
  * own Esc, the part removed, another figure opened -- it closes and lets go of the preview it held.
@@ -323,7 +323,7 @@ export class FigureEditor extends LitElement {
323
323
  if (mode === 'scale' && this.selected && this.gizmoMode !== 'scale') {
324
324
  const asset = this.asset()
325
325
  if (asset && this.sizeDragUnsure(asset, this.selected)) {
326
- this.error = i18next.t('figure.text.gizmo-size-turned-fastening', { part: partLabelOf(this.selected), extreme: this.keptExtremeWord() })
326
+ this.tellSizeByFields(this.selected)
327
327
  return
328
328
  }
329
329
  }
@@ -579,6 +579,28 @@ export class FigureEditor extends LitElement {
579
579
  return true
580
580
  }
581
581
  }
582
+ /**
583
+ * Why this part's size is not dragged, in one line (user 2026-10-19: the reason told the faces kept, and was not
584
+ * understood): attached turned or twisted, or turned off its upright.
585
+ */
586
+ private sizeDragRefusalOf(id: string): string {
587
+ const fastened = !!this.asset()?.joints?.some(j => j.body1.part === ownerOf(id))
588
+ return i18next.t(fastened ? 'figure.text.gizmo-size-turned-fastening' : 'figure.text.gizmo-size-tilted', { part: partLabelOf(id) })
589
+ }
590
+ /** The size drag refused, said as a notice with the way to the part's size fields. */
591
+ private tellSizeByFields(id: string) {
592
+ this.refusals.point(this.sizeDragRefusalOf(id), { label: i18next.t('figure.button.to-size-fields'), run: () => this.toSizeFields(id) })
593
+ }
594
+ /** To the part's first size field it takes typed, on the properties tab, ready to type over. */
595
+ private async toSizeFields(id: string) {
596
+ this.middle = 'edit'
597
+ this.tab = 'properties'
598
+ this.selected = id
599
+ await this.updateComplete
600
+ const field = this.renderRoot.querySelector<HTMLInputElement>('[slot="size"] input:not([disabled]):not([readonly])')
601
+ field?.focus()
602
+ field?.select()
603
+ }
582
604
  /** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
583
605
  private transformed(id: string, result: GizmoResult) {
584
606
  const view = this.session!.read()
@@ -605,7 +627,7 @@ export class FigureEditor extends LitElement {
605
627
  const jointed = !!asset.joints?.some(j => j.body1.part === ownerOf(id))
606
628
  /* A size drag this part cannot show truly is not written: it springs back, and the reason is said. */
607
629
  if (result.scale && result.scale.some(k => Math.abs(k - 1) > 1e-4) && this.sizeDragUnsure(asset, id)) {
608
- this.error = i18next.t('figure.text.gizmo-size-turned-fastening', { part: partLabelOf(id), extreme: this.keptExtremeWord() })
630
+ this.tellSizeByFields(id)
609
631
  return this.requestUpdate()
610
632
  }
611
633
  /*
@@ -677,10 +699,6 @@ export class FigureEditor extends LitElement {
677
699
  * fastened. Refused, it says why.
678
700
  */
679
701
  /* What a tilted part's typed size keeps, by the figure's placement: not the floor's lowest point everywhere (designer 2026-09-27). */
680
- private keptExtremeWord(): string {
681
- const placement = (this.asset())?.placement ?? 'floor'
682
- return i18next.t(`figure.label.kept-extreme-${placement}`)
683
- }
684
702
  /* How the selected part would meet the plane, worked out once per version and part. */
685
703
  private planeContactOf(shown: FigureRevision, part: string): FigurePlaneContact | undefined {
686
704
  return derivedOf(shown, `plane-contact|${part}`, () => {
@@ -883,6 +901,7 @@ export class FigureEditor extends LitElement {
883
901
  private renderOverlay() {
884
902
  const close = () => (this.overlay = undefined)
885
903
  const panel = (which: BarOverlay, label: string, body: unknown) => html`<div
904
+ side-overlay
886
905
  bar-overlay-panel=${which}
887
906
  role="dialog"
888
907
  aria-label=${i18next.t(label)}
@@ -1448,48 +1467,44 @@ export class FigureEditor extends LitElement {
1448
1467
  this.openFastening()
1449
1468
  }
1450
1469
  private openFastening() {
1451
- if (!this.selected || this.fastenPopup) return
1470
+ if (!this.selected || this.fastening) return
1452
1471
  this.fastenError = ''
1453
1472
  this.fastenHover = null
1473
+ /* One panel over the side at a time: the view or the history gives way to the window. */
1474
+ this.overlay = undefined
1454
1475
  this.fastening = true
1455
- const content = document.createElement('figure-fasten-dialog') as NonNullable<typeof this.fastenWindow>
1456
- content.addEventListener('preview-command', e => this.commandPreview.ask((e as CustomEvent).detail))
1457
- content.addEventListener('fasten-hover', e => this.hoverFastening((e as CustomEvent).detail))
1458
- content.addEventListener('fasten-apply', e => this.applyFastening((e as CustomEvent).detail.action))
1459
- content.addEventListener('fasten-cancel', () => this.closeFastening())
1460
- this.fastenWindow = content
1461
- this.tellFastening()
1462
- const popup = openPopup(html`${content}`, { title: i18next.t('figure.label.fastening'), size: 'fit', backdrop: true, movable: true, closable: true, escapable: true })
1463
- this.fastenPopup = popup
1464
- /* Closed by its own X or Esc as well: the editor lets go of it the same way. */
1465
- popup.result.then(() => this.fastenPopup === popup && this.fastenClosed())
1466
1476
  }
1467
1477
  private closeFastening() {
1468
- const popup = this.fastenPopup
1469
- this.fastenClosed()
1470
- popup?.close()
1471
- }
1472
- private fastenClosed() {
1473
- this.fastenPopup = undefined
1474
- this.fastenWindow = undefined
1475
1478
  this.fastening = false
1476
1479
  this.fastenError = ''
1477
1480
  this.fastenHover = null
1478
1481
  }
1479
- /** What the window shows is the editor's: the figure on screen, the part, and what the preview and a refusal said. */
1480
- private tellFastening() {
1481
- const content = this.fastenWindow
1482
- const view = this.session?.read()
1483
- if (!content || !view) return
1484
- content.revision = this.shown()
1485
- content.partId = this.selected
1486
- content.locked = view.loading || !!view.error || view.saving || !view.context
1487
- content.previewWords = this.commandPreview.words()
1488
- content.error = this.fastenError
1489
- }
1490
- disconnectedCallback() {
1491
- super.disconnectedCallback()
1492
- this.closeFastening()
1482
+ /**
1483
+ * The fastening window over the side panel, as the bar's panels stand there: the figure on screen, the part, and what
1484
+ * the preview and a refusal said are the editor's. Its own Esc and 「취소」 close it, as its X does.
1485
+ */
1486
+ private renderFastening(unavailable: boolean) {
1487
+ if (!this.fastening || !this.selected) return nothing
1488
+ return html`<div side-overlay fasten-panel>
1489
+ <div overlay-head>
1490
+ <h3>${i18next.t('figure.label.fastening')}</h3>
1491
+ <button quiet fasten-close aria-label=${i18next.t('figure.button.close')} title=${i18next.t('figure.button.close')} @click=${() => this.closeFastening()}>
1492
+ <md-icon>close</md-icon>
1493
+ </button>
1494
+ </div>
1495
+ <figure-fasten-dialog
1496
+ overlay-body
1497
+ .revision=${this.shown()}
1498
+ .partId=${this.selected}
1499
+ ?locked=${unavailable}
1500
+ .previewWords=${this.commandPreview.words()}
1501
+ .error=${this.fastenError}
1502
+ @preview-command=${(e: CustomEvent) => this.commandPreview.ask(e.detail)}
1503
+ @fasten-hover=${(e: CustomEvent) => this.hoverFastening(e.detail)}
1504
+ @fasten-apply=${(e: CustomEvent) => this.applyFastening(e.detail.action)}
1505
+ @fasten-cancel=${() => this.closeFastening()}
1506
+ ></figure-fasten-dialog>
1507
+ </div>`
1493
1508
  }
1494
1509
  /** 「적용」: the command, one step; refused, the window stays open and says why. */
1495
1510
  private applyFastening(action: FigureAuthoringAction) {
@@ -1557,7 +1572,6 @@ export class FigureEditor extends LitElement {
1557
1572
  }
1558
1573
  updated(changed: PropertyValues<this>) {
1559
1574
  super.updated?.(changed)
1560
- this.tellFastening()
1561
1575
  /* Nothing blocks release any more: the list under the bar closes, and opens fresh when something does again. */
1562
1576
  if (changed.has('releaseBlockers') && !this.releaseBlockers.length && this.blockersOpen) this.blockersOpen = false
1563
1577
  this.refusals.tell(changed)
@@ -1945,7 +1959,7 @@ export class FigureEditor extends LitElement {
1945
1959
  <div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
1946
1960
  </aside>
1947
1961
  </ox-three-pane>
1948
- ${this.renderOverlay()}`
1962
+ ${this.renderOverlay()} ${this.renderFastening(unavailable)}`
1949
1963
  }
1950
1964
  /** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
1951
1965
  private renderGroups(
@@ -30,9 +30,9 @@ import type { FigureRevision } from './revision.js'
30
30
  const PLANE = '__plane'
31
31
 
32
32
  /**
33
- * 붙이기 창: what the selected part is fastened to, on which face, how -- chosen with room to see it, in the standard
34
- * popup the editor opens (`openPopup`: its title, its close, moved by its head) (user 2026-10-17: 「속성에는 심플하고
35
- * 직관적인 결과 위주로, 편집은 팝업에서 넓은 공간을 써서」; 2026-10-18: the standard popup, not a window of its own).
33
+ * 붙이기 창: what the selected part is fastened to, on which face, how (user 2026-10-17: 「속성에는 심플하고 직관적인
34
+ * 결과 위주로, 편집은 팝업에서 넓은 공간을 써서」). The editor stands it over the side panel with its title and close,
35
+ * where the canvas stays in sight (2026-10-19: the standard popup, always modal, hid the 3D it was choosing for).
36
36
  *
37
37
  * Everything chosen here is tried, not done: the command it comes to is told to the editor as it changes
38
38
  * (`preview-command`), and the canvas draws it. 「적용」 sends it once (`fasten-apply`), one step of the history;
@@ -104,6 +104,14 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
104
104
  margin: 0;
105
105
  color: var(--md-sys-color-error);
106
106
  }
107
+ /* 취소 · 적용 stay in sight at the window's foot when what is above them is taller than the panel. */
108
+ ox-popup-actions {
109
+ position: sticky;
110
+ bottom: calc(-1 * var(--popup-padding, 16px));
111
+ margin-bottom: calc(-1 * var(--popup-padding, 16px));
112
+ padding-bottom: var(--popup-padding, 16px);
113
+ background: var(--md-sys-color-surface-container-lowest);
114
+ }
107
115
  `
108
116
  ]
109
117
 
@@ -410,8 +418,6 @@ export class FigureFastenDialog extends localize(i18next)(LitElement) {
410
418
  this.setAttribute('role', 'dialog')
411
419
  this.setAttribute('aria-label', i18next.t('figure.label.fastening'))
412
420
  this.addEventListener('keydown', this.onKey)
413
- /* The popup's fitting width is the overlay's own setting; the window asks it for room for the face and the way side by side. */
414
- this.closest<HTMLElement>('ox-floating-overlay')?.style.setProperty('--overlay-center-fit-width', '640px')
415
421
  }
416
422
 
417
423
  private onKey = async (e: KeyboardEvent) => {
@@ -47,6 +47,14 @@ export class RefusalTeller {
47
47
  this.toldViewError = viewError
48
48
  }
49
49
 
50
+ /**
51
+ * What cannot be done this way, with the way it can, as a notice: nothing went wrong, so not the error's red
52
+ * (user 2026-10-19, the size drag on a part attached turned). Given more time than a plain notice, for its button.
53
+ */
54
+ point(message: string, way: { label: string; run: () => void }) {
55
+ notify({ level: 'info', message, option: { action: { label: way.label, callback: way.run }, timer: 8000 } })
56
+ }
57
+
50
58
  /*
51
59
  An edit that would stop clips is refused by figure-model with the clips named (CLIPS_DROPPED). The person is told
52
60
  which, and why, before anything is written; agreed, the same edit is made again letting exactly those go, as one
@@ -294,12 +294,13 @@ export const EditorStyles = css `
294
294
  }
295
295
  /*
296
296
  The view settings and the release history, opened from the bar over the right of the work: neither is about the
297
- part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16).
297
+ part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16). The fastening window stands in
298
+ the same place, over the side panel, so the canvas it previews on stays in sight (user 2026-10-19).
298
299
  */
299
300
  :host {
300
301
  position: relative;
301
302
  }
302
- div[bar-overlay-panel] {
303
+ div[side-overlay] {
303
304
  position: absolute;
304
305
  top: var(--figure-bar-height, 44px);
305
306
  right: 8px;
@@ -326,10 +327,16 @@ export const EditorStyles = css `
326
327
  div[overlay-head] button[quiet] {
327
328
  margin-left: 0;
328
329
  }
329
- div[overlay-body] {
330
+ [overlay-body] {
330
331
  min-height: 0;
331
332
  overflow: auto;
332
333
  }
334
+ /* The fastening window keeps to the panel's width: a popup's own sizing made it wider than the panel. */
335
+ div[fasten-panel] > figure-fasten-dialog {
336
+ box-sizing: border-box;
337
+ width: 100%;
338
+ min-width: 0;
339
+ }
333
340
  /* Which part the panel speaks of. */
334
341
  div[subject] {
335
342
  position: sticky;
@@ -1 +1 @@
1
- {"version":3,"file":"editor-styles.js","sourceRoot":"","sources":["../../client/modeller/editor-styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,gHAAgH;AAChH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoa9B,CAAA","sourcesContent":["import { css } from 'lit'\n\n/** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */\nexport const EditorStyles = css`\n :host {\n position: relative;\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */\n container-type: inline-size;\n /* The app's one face (its typography theme's --theme-font) at the body size: a bare sans-serif here put a second\n face under everything the editor draws, beside the parts list and the shell in the app's own (2026-10-16). */\n font-family: var(--theme-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-t-body, 13px);\n color: var(--md-sys-color-on-surface);\n }\n /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and\n fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */\n ox-canvas-bar button[mode] {\n height: 28px;\n border: 1px solid transparent;\n padding: 0 10px;\n border-radius: 999em;\n border-color: transparent;\n background: none;\n }\n ox-canvas-bar button[mode][aria-pressed='true'] {\n border-color: var(--md-sys-color-primary);\n }\n /* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */\n ox-canvas-bar .gizmo-tools {\n display: inline-flex;\n gap: 2px;\n padding-left: 4px;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ox-canvas-bar button[tool] {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n ox-canvas-bar button[tool]:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n }\n ox-canvas-bar button[tool][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n ox-canvas-bar button[tool]:disabled {\n opacity: 0.38;\n cursor: default;\n }\n /*\n Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they\n showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.\n */\n ox-canvas-bar button[tool] md-icon {\n --md-icon-size: 20px;\n }\n ox-canvas-bar button[tool]:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n /* The edit canvas stays standing while the preview is in front (a 3D scene costs a third of a second to make), out of sight. */\n div[middle][hidden] {\n display: none;\n }\n /* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */\n figure-preview[hidden] {\n display: none;\n }\n ox-canvas-bar .status {\n padding: 0 8px;\n color: var(--md-sys-color-on-surface-variant);\n }\n ox-canvas-bar .status[dirty] {\n color: var(--md-sys-color-primary);\n font-weight: 600;\n }\n /*\n The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):\n round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,\n a promise to whoever draws the figure, is outlined.\n */\n ox-canvas-bar button[save],\n ox-canvas-bar button[publish] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 14px 0 10px;\n border-radius: 999em;\n font-weight: 600;\n }\n ox-canvas-bar button[save] md-icon,\n ox-canvas-bar button[publish] md-icon {\n --md-icon-size: 18px;\n }\n ox-canvas-bar button[save] {\n border: 1px solid transparent;\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n }\n /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under\n 860px and one column with the middle first under 560px are the frame's, as are the dividers. */\n ox-three-pane {\n flex: 1;\n min-height: 0;\n }\n figure-parts {\n min-height: 0;\n }\n figure-canvas {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n div[middle] {\n position: relative;\n display: flex;\n min-width: 0;\n min-height: 0;\n }\n /* A new figure's first question, over the empty canvas. */\n div[start] {\n position: absolute;\n inset: 50% auto auto 50%;\n z-index: 10;\n color: var(--md-sys-color-on-surface);\n transform: translate(-50%, -50%);\n width: min(440px, calc(100% - 32px));\n box-sizing: border-box;\n padding: 20px;\n border-radius: 12px;\n background: var(--md-sys-color-surface, #fff);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);\n }\n div[start] button[start-button] {\n width: auto;\n margin: 0;\n font-size: var(--ui-t-body);\n height: 36px;\n padding: 0 16px;\n border: 1px solid var(--md-sys-color-outline);\n border-radius: 999em;\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface, #fff);\n cursor: pointer;\n }\n div[start] button[start-button]:hover:not(:disabled) {\n background: var(--md-sys-color-primary-container);\n }\n div[start] button[start-button][pressed] {\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border-color: var(--md-sys-color-primary);\n }\n div[start] button[start-button]:disabled {\n opacity: 0.5;\n cursor: default;\n }\n div[start] h2 {\n margin: 0 0 12px;\n font-size: var(--ui-t-title);\n }\n div[start-paths] {\n display: grid;\n gap: 14px;\n }\n div[start-paths] h3 {\n margin: 0 0 6px;\n font-size: var(--ui-t-body);\n }\n div[start-shapes] {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n div[start] small {\n display: block;\n margin-top: 4px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The side: tabs over one panel at a time, each using the whole height. */\n /*\n The side's words at the panel's size (side-panel-styles: titles body, the rest small). The editor's host is at body\n for the bar, and the fields the editor drew itself -- the part's name -- stood a step larger than every other field.\n */\n aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\n font-size: var(--ui-t-small, 11.5px);\n background: var(--md-sys-color-surface-container-lowest);\n }\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n min-width: 0;\n height: 48px;\n padding: 0;\n border: none;\n border-radius: 0;\n border-bottom: 2px solid transparent;\n background: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n button[tab]:hover:not([on]) {\n background: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n button[tab] span[tab-label] {\n max-width: 100%;\n overflow: hidden;\n font-size: var(--ui-t-micro, 11px);\n line-height: 1.2;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n span[badge] {\n position: absolute;\n top: 3px;\n right: 2px;\n padding: 0 5px;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n line-height: 1.5;\n color: var(--md-sys-color-on-primary-container);\n background: var(--md-sys-color-primary-container);\n border-radius: 999em;\n pointer-events: none;\n }\n /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */\n div[panel] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 0;\n }\n div[panel][flush] {\n padding: 0;\n }\n /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */\n [off] {\n display: none !important;\n }\n /* The part's own numbers inside a group the links draw (size, place): one column, the panel's gap. */\n div[group-fields] {\n display: grid;\n gap: 6px;\n }\n /* The folded 「더 보기」: its title is the fold's handle, at the title's size. */\n section[more] > details > summary {\n padding: 0;\n }\n section[more] > details > summary > h3 {\n margin: 0;\n }\n div[more-body] {\n display: grid;\n gap: 16px;\n margin-top: 12px;\n }\n /*\n The view settings and the release history, opened from the bar over the right of the work: neither is about the\n part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16).\n */\n :host {\n position: relative;\n }\n div[bar-overlay-panel] {\n position: absolute;\n top: var(--figure-bar-height, 44px);\n right: 8px;\n z-index: 5;\n display: flex;\n flex-direction: column;\n width: min(360px, calc(100% - 16px));\n max-height: calc(100% - var(--figure-bar-height, 44px) - 16px);\n background: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);\n }\n div[overlay-head] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 8px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[overlay-head] h3 {\n margin: 0;\n }\n div[overlay-head] button[quiet] {\n margin-left: 0;\n }\n div[overlay-body] {\n min-height: 0;\n overflow: auto;\n }\n /* Which part the panel speaks of. */\n div[subject] {\n position: sticky;\n top: 0;\n z-index: 1;\n display: grid;\n gap: 4px;\n padding: 10px 12px;\n background: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */\n div[subject-summary] {\n display: grid;\n gap: 2px;\n }\n div[subject-summary] b {\n font-size: var(--ui-t-body, 13px);\n font-weight: 600;\n }\n div[subject-summary] b span {\n font-weight: 400;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[subject-summary] small {\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n No rule here draws every button as a box: the side's buttons take the shared panel rules (SidePanelStyles), one\n height with the fields beside them, and only the bar's publish keeps an outline (user 2026-10-16: 「버튼 박스도\n 과하게 사용되어서」 -- a bare 「button」 rule had put a 32px box round every button the editor drew).\n */\n button {\n font: inherit;\n box-sizing: border-box;\n color: inherit;\n min-width: 0;\n cursor: pointer;\n }\n /* What blocks release, beside 「발행」: one red word button, and its list under the bar. */\n ox-canvas-bar button[release-blocked] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 32px;\n padding: 0 14px 0 10px;\n border: none;\n /* Round as save and release beside it: one shape for the bar's end. */\n border-radius: 999em;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n }\n ox-canvas-bar button[release-blocked] md-icon {\n --md-icon-size: 18px;\n }\n div[release-blockers] {\n position: absolute;\n top: 46px;\n right: 8px;\n z-index: 6;\n width: min(340px, calc(100% - 16px));\n padding: 8px 10px 10px;\n box-sizing: border-box;\n background: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);\n }\n ox-canvas-bar button[publish] {\n border: 1px solid var(--md-sys-color-outline);\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface);\n }\n ox-canvas-bar button[publish][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n button:disabled {\n opacity: 0.45;\n cursor: default;\n }\n button[aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n border-color: var(--md-sys-color-primary);\n }\n .hint {\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n`\n"]}
1
+ {"version":3,"file":"editor-styles.js","sourceRoot":"","sources":["../../client/modeller/editor-styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,gHAAgH;AAChH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2a9B,CAAA","sourcesContent":["import { css } from 'lit'\n\n/** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */\nexport const EditorStyles = css`\n :host {\n position: relative;\n display: flex;\n flex-direction: column;\n min-height: 0;\n height: 100%;\n /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */\n container-type: inline-size;\n /* The app's one face (its typography theme's --theme-font) at the body size: a bare sans-serif here put a second\n face under everything the editor draws, beside the parts list and the shell in the app's own (2026-10-16). */\n font-family: var(--theme-font, ui-sans-serif, system-ui, sans-serif);\n font-size: var(--ui-t-body, 13px);\n color: var(--md-sys-color-on-surface);\n }\n /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and\n fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */\n ox-canvas-bar button[mode] {\n height: 28px;\n border: 1px solid transparent;\n padding: 0 10px;\n border-radius: 999em;\n border-color: transparent;\n background: none;\n }\n ox-canvas-bar button[mode][aria-pressed='true'] {\n border-color: var(--md-sys-color-primary);\n }\n /* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */\n ox-canvas-bar .gizmo-tools {\n display: inline-flex;\n gap: 2px;\n padding-left: 4px;\n border-left: 1px solid var(--md-sys-color-outline-variant);\n }\n ox-canvas-bar button[tool] {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: var(--md-sys-shape-corner-small, 8px);\n background: transparent;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n ox-canvas-bar button[tool]:hover:not(:disabled) {\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface);\n }\n ox-canvas-bar button[tool][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n ox-canvas-bar button[tool]:disabled {\n opacity: 0.38;\n cursor: default;\n }\n /*\n Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they\n showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.\n */\n ox-canvas-bar button[tool] md-icon {\n --md-icon-size: 20px;\n }\n ox-canvas-bar button[tool]:focus-visible {\n outline: 2px solid var(--md-sys-color-primary);\n outline-offset: 1px;\n }\n figure-preview {\n min-width: 0;\n min-height: 0;\n }\n /* The edit canvas stays standing while the preview is in front (a 3D scene costs a third of a second to make), out of sight. */\n div[middle][hidden] {\n display: none;\n }\n /* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */\n figure-preview[hidden] {\n display: none;\n }\n ox-canvas-bar .status {\n padding: 0 8px;\n color: var(--md-sys-color-on-surface-variant);\n }\n ox-canvas-bar .status[dirty] {\n color: var(--md-sys-color-primary);\n font-weight: 600;\n }\n /*\n The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):\n round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,\n a promise to whoever draws the figure, is outlined.\n */\n ox-canvas-bar button[save],\n ox-canvas-bar button[publish] {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 14px 0 10px;\n border-radius: 999em;\n font-weight: 600;\n }\n ox-canvas-bar button[save] md-icon,\n ox-canvas-bar button[publish] md-icon {\n --md-icon-size: 18px;\n }\n ox-canvas-bar button[save] {\n border: 1px solid transparent;\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n }\n /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under\n 860px and one column with the middle first under 560px are the frame's, as are the dividers. */\n ox-three-pane {\n flex: 1;\n min-height: 0;\n }\n figure-parts {\n min-height: 0;\n }\n figure-canvas {\n flex: 1;\n min-width: 0;\n min-height: 0;\n }\n div[middle] {\n position: relative;\n display: flex;\n min-width: 0;\n min-height: 0;\n }\n /* A new figure's first question, over the empty canvas. */\n div[start] {\n position: absolute;\n inset: 50% auto auto 50%;\n z-index: 10;\n color: var(--md-sys-color-on-surface);\n transform: translate(-50%, -50%);\n width: min(440px, calc(100% - 32px));\n box-sizing: border-box;\n padding: 20px;\n border-radius: 12px;\n background: var(--md-sys-color-surface, #fff);\n box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);\n }\n div[start] button[start-button] {\n width: auto;\n margin: 0;\n font-size: var(--ui-t-body);\n height: 36px;\n padding: 0 16px;\n border: 1px solid var(--md-sys-color-outline);\n border-radius: 999em;\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface, #fff);\n cursor: pointer;\n }\n div[start] button[start-button]:hover:not(:disabled) {\n background: var(--md-sys-color-primary-container);\n }\n div[start] button[start-button][pressed] {\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border-color: var(--md-sys-color-primary);\n }\n div[start] button[start-button]:disabled {\n opacity: 0.5;\n cursor: default;\n }\n div[start] h2 {\n margin: 0 0 12px;\n font-size: var(--ui-t-title);\n }\n div[start-paths] {\n display: grid;\n gap: 14px;\n }\n div[start-paths] h3 {\n margin: 0 0 6px;\n font-size: var(--ui-t-body);\n }\n div[start-shapes] {\n display: flex;\n gap: 8px;\n flex-wrap: wrap;\n }\n div[start] small {\n display: block;\n margin-top: 4px;\n color: var(--md-sys-color-on-surface-variant);\n }\n /* The side: tabs over one panel at a time, each using the whole height. */\n /*\n The side's words at the panel's size (side-panel-styles: titles body, the rest small). The editor's host is at body\n for the bar, and the fields the editor drew itself -- the part's name -- stood a step larger than every other field.\n */\n aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\n font-size: var(--ui-t-small, 11.5px);\n background: var(--md-sys-color-surface-container-lowest);\n }\n div[tabs] {\n display: flex;\n flex: none;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n button[tab] {\n flex: 1;\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 1px;\n min-width: 0;\n height: 48px;\n padding: 0;\n border: none;\n border-radius: 0;\n border-bottom: 2px solid transparent;\n background: none;\n color: var(--md-sys-color-on-surface-variant);\n }\n button[tab]:hover:not([on]) {\n background: var(--md-sys-color-surface-container);\n }\n button[tab][on] {\n color: var(--md-sys-color-primary);\n border-bottom-color: var(--md-sys-color-primary);\n }\n button[tab] md-icon {\n font-size: 20px;\n width: 20px;\n height: 20px;\n }\n button[tab] span[tab-label] {\n max-width: 100%;\n overflow: hidden;\n font-size: var(--ui-t-micro, 11px);\n line-height: 1.2;\n white-space: nowrap;\n text-overflow: ellipsis;\n }\n span[badge] {\n position: absolute;\n top: 3px;\n right: 2px;\n padding: 0 5px;\n font-size: var(--ui-t-micro, 11px);\n font-weight: 700;\n line-height: 1.5;\n color: var(--md-sys-color-on-primary-container);\n background: var(--md-sys-color-primary-container);\n border-radius: 999em;\n pointer-events: none;\n }\n /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */\n div[panel] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 0;\n }\n div[panel][flush] {\n padding: 0;\n }\n /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */\n [off] {\n display: none !important;\n }\n /* The part's own numbers inside a group the links draw (size, place): one column, the panel's gap. */\n div[group-fields] {\n display: grid;\n gap: 6px;\n }\n /* The folded 「더 보기」: its title is the fold's handle, at the title's size. */\n section[more] > details > summary {\n padding: 0;\n }\n section[more] > details > summary > h3 {\n margin: 0;\n }\n div[more-body] {\n display: grid;\n gap: 16px;\n margin-top: 12px;\n }\n /*\n The view settings and the release history, opened from the bar over the right of the work: neither is about the\n part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16). The fastening window stands in\n the same place, over the side panel, so the canvas it previews on stays in sight (user 2026-10-19).\n */\n :host {\n position: relative;\n }\n div[side-overlay] {\n position: absolute;\n top: var(--figure-bar-height, 44px);\n right: 8px;\n z-index: 5;\n display: flex;\n flex-direction: column;\n width: min(360px, calc(100% - 16px));\n max-height: calc(100% - var(--figure-bar-height, 44px) - 16px);\n background: var(--md-sys-color-surface-container-lowest);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);\n }\n div[overlay-head] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 8px 8px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n div[overlay-head] h3 {\n margin: 0;\n }\n div[overlay-head] button[quiet] {\n margin-left: 0;\n }\n [overlay-body] {\n min-height: 0;\n overflow: auto;\n }\n /* The fastening window keeps to the panel's width: a popup's own sizing made it wider than the panel. */\n div[fasten-panel] > figure-fasten-dialog {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n }\n /* Which part the panel speaks of. */\n div[subject] {\n position: sticky;\n top: 0;\n z-index: 1;\n display: grid;\n gap: 4px;\n padding: 10px 12px;\n background: var(--md-sys-color-surface-container-lowest);\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */\n div[subject-summary] {\n display: grid;\n gap: 2px;\n }\n div[subject-summary] b {\n font-size: var(--ui-t-body, 13px);\n font-weight: 600;\n }\n div[subject-summary] b span {\n font-weight: 400;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[subject-summary] small {\n color: var(--md-sys-color-on-surface-variant);\n }\n /*\n No rule here draws every button as a box: the side's buttons take the shared panel rules (SidePanelStyles), one\n height with the fields beside them, and only the bar's publish keeps an outline (user 2026-10-16: 「버튼 박스도\n 과하게 사용되어서」 -- a bare 「button」 rule had put a 32px box round every button the editor drew).\n */\n button {\n font: inherit;\n box-sizing: border-box;\n color: inherit;\n min-width: 0;\n cursor: pointer;\n }\n /* What blocks release, beside 「발행」: one red word button, and its list under the bar. */\n ox-canvas-bar button[release-blocked] {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n height: 32px;\n padding: 0 14px 0 10px;\n border: none;\n /* Round as save and release beside it: one shape for the bar's end. */\n border-radius: 999em;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n }\n ox-canvas-bar button[release-blocked] md-icon {\n --md-icon-size: 18px;\n }\n div[release-blockers] {\n position: absolute;\n top: 46px;\n right: 8px;\n z-index: 6;\n width: min(340px, calc(100% - 16px));\n padding: 8px 10px 10px;\n box-sizing: border-box;\n background: var(--md-sys-color-surface);\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);\n }\n ox-canvas-bar button[publish] {\n border: 1px solid var(--md-sys-color-outline);\n color: var(--md-sys-color-primary);\n background: var(--md-sys-color-surface);\n }\n ox-canvas-bar button[publish][aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n button:disabled {\n opacity: 0.45;\n cursor: default;\n }\n button[aria-pressed='true'] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n border-color: var(--md-sys-color-primary);\n }\n .hint {\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n`\n"]}
@@ -65,12 +65,12 @@ export declare class FigureEditor extends LitElement {
65
65
  /** The view settings or the release history, open over the work from the bar. */
66
66
  private overlay?;
67
67
  /**
68
- * The fastening window open for the selected part (user 2026-10-17), in the standard popup (2026-10-18): chosen there,
69
- * applied once. It closes with nothing written when the part, the tab or the middle changes under it.
68
+ * The fastening window open for the selected part (user 2026-10-17): chosen there, applied once. It stands over the
69
+ * side panel, not over the canvas, so what is chosen is seen in 3D as it is chosen (user 2026-10-19: the standard
70
+ * popup, always modal, covered the preview). It closes with nothing written when the part, the tab, the middle or a
71
+ * bar panel changes under it.
70
72
  */
71
73
  private fastening;
72
- private fastenPopup?;
73
- private fastenWindow?;
74
74
  /** Why the window's 「적용」 was refused, said in it. */
75
75
  private fastenError;
76
76
  /** The face of the part a row of the window's list is on, lit on the canvas while the pointer or the arrows are there. */
@@ -185,6 +185,15 @@ export declare class FigureEditor extends LitElement {
185
185
  private reachOf;
186
186
  /** Whether a size drag on this part could not show what is written: figure-model gives no face it keeps. */
187
187
  private sizeDragUnsure;
188
+ /**
189
+ * Why this part's size is not dragged, in one line (user 2026-10-19: the reason told the faces kept, and was not
190
+ * understood): attached turned or twisted, or turned off its upright.
191
+ */
192
+ private sizeDragRefusalOf;
193
+ /** The size drag refused, said as a notice with the way to the part's size fields. */
194
+ private tellSizeByFields;
195
+ /** To the part's first size field it takes typed, on the properties tab, ready to type over. */
196
+ private toSizeFields;
188
197
  /** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
189
198
  private transformed;
190
199
  /** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
@@ -197,7 +206,6 @@ export declare class FigureEditor extends LitElement {
197
206
  * its top, a wall one's to the wall by its back, a centred one's centre to the origin's height. Once: nothing is
198
207
  * fastened. Refused, it says why.
199
208
  */
200
- private keptExtremeWord;
201
209
  private planeContactOf;
202
210
  private renderPlaneAlign;
203
211
  private alignToPlane;
@@ -307,10 +315,11 @@ export declare class FigureEditor extends LitElement {
307
315
  private goFasten;
308
316
  private openFastening;
309
317
  private closeFastening;
310
- private fastenClosed;
311
- /** What the window shows is the editor's: the figure on screen, the part, and what the preview and a refusal said. */
312
- private tellFastening;
313
- disconnectedCallback(): void;
318
+ /**
319
+ * The fastening window over the side panel, as the bar's panels stand there: the figure on screen, the part, and what
320
+ * the preview and a refusal said are the editor's. Its own Esc and 「취소」 close it, as its X does.
321
+ */
322
+ private renderFastening;
314
323
  /** 「적용」: the command, one step; refused, the window stays open and says why. */
315
324
  private applyFastening;
316
325
  /** A row of the window's list under the pointer or the arrows: the face of that part it would be fastened on, lit. */
@@ -80,8 +80,10 @@ let FigureEditor = class FigureEditor extends LitElement {
80
80
  this.error = '';
81
81
  this.tab = 'properties';
82
82
  /**
83
- * The fastening window open for the selected part (user 2026-10-17), in the standard popup (2026-10-18): chosen there,
84
- * applied once. It closes with nothing written when the part, the tab or the middle changes under it.
83
+ * The fastening window open for the selected part (user 2026-10-17): chosen there, applied once. It stands over the
84
+ * side panel, not over the canvas, so what is chosen is seen in 3D as it is chosen (user 2026-10-19: the standard
85
+ * popup, always modal, covered the preview). It closes with nothing written when the part, the tab, the middle or a
86
+ * bar panel changes under it.
85
87
  */
86
88
  this.fastening = false;
87
89
  /** Why the window's 「적용」 was refused, said in it. */
@@ -219,7 +221,7 @@ let FigureEditor = class FigureEditor extends LitElement {
219
221
  aria-pressed=${String(this.gizmoMode === tool.mode)}
220
222
  ?disabled=${!tool.can}
221
223
  aria-label=${i18next.t('figure.button.gizmo-' + tool.mode)}
222
- title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + i18next.t(tool.mode === 'scale' && this.sizeDragUnsure(this.asset(), this.selected) ? 'figure.text.gizmo-size-turned-fastening' : 'figure.text.gizmo-not-own', { part: partLabelOf(this.selected), extreme: this.keptExtremeWord() })}`}
224
+ title=${`${i18next.t('figure.button.gizmo-' + tool.mode)} (${tool.key})${tool.can || !this.selected ? '' : ' · ' + (tool.mode === 'scale' && this.sizeDragUnsure(this.asset(), this.selected) ? this.sizeDragRefusalOf(this.selected) : i18next.t('figure.text.gizmo-not-own'))}`}
223
225
  @click=${() => this.useGizmo(tool.mode)}
224
226
  >
225
227
  <md-icon>${tool.icon}</md-icon>
@@ -261,7 +263,7 @@ let FigureEditor = class FigureEditor extends LitElement {
261
263
  if (_changed.has('selected'))
262
264
  this.gizmoMode = undefined;
263
265
  /* The window is about the part chosen on the properties tab, being edited: it does not follow anywhere else. */
264
- if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit'))
266
+ if (this.fastening && (_changed.has('selected') || !this.selected || this.tab !== 'properties' || this.middle !== 'edit' || this.overlay))
265
267
  this.closeFastening();
266
268
  /*
267
269
  * The section editor is of one part: when the selection moves off it by any way -- another part picked, the scene's
@@ -295,7 +297,7 @@ let FigureEditor = class FigureEditor extends LitElement {
295
297
  if (mode === 'scale' && this.selected && this.gizmoMode !== 'scale') {
296
298
  const asset = this.asset();
297
299
  if (asset && this.sizeDragUnsure(asset, this.selected)) {
298
- this.error = i18next.t('figure.text.gizmo-size-turned-fastening', { part: partLabelOf(this.selected), extreme: this.keptExtremeWord() });
300
+ this.tellSizeByFields(this.selected);
299
301
  return;
300
302
  }
301
303
  }
@@ -575,6 +577,28 @@ let FigureEditor = class FigureEditor extends LitElement {
575
577
  return true;
576
578
  }
577
579
  }
580
+ /**
581
+ * Why this part's size is not dragged, in one line (user 2026-10-19: the reason told the faces kept, and was not
582
+ * understood): attached turned or twisted, or turned off its upright.
583
+ */
584
+ sizeDragRefusalOf(id) {
585
+ const fastened = !!this.asset()?.joints?.some(j => j.body1.part === ownerOf(id));
586
+ return i18next.t(fastened ? 'figure.text.gizmo-size-turned-fastening' : 'figure.text.gizmo-size-tilted', { part: partLabelOf(id) });
587
+ }
588
+ /** The size drag refused, said as a notice with the way to the part's size fields. */
589
+ tellSizeByFields(id) {
590
+ this.refusals.point(this.sizeDragRefusalOf(id), { label: i18next.t('figure.button.to-size-fields'), run: () => this.toSizeFields(id) });
591
+ }
592
+ /** To the part's first size field it takes typed, on the properties tab, ready to type over. */
593
+ async toSizeFields(id) {
594
+ this.middle = 'edit';
595
+ this.tab = 'properties';
596
+ this.selected = id;
597
+ await this.updateComplete;
598
+ const field = this.renderRoot.querySelector('[slot="size"] input:not([disabled]):not([readonly])');
599
+ field?.focus();
600
+ field?.select();
601
+ }
578
602
  /** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
579
603
  transformed(id, result) {
580
604
  const view = this.session.read();
@@ -603,7 +627,7 @@ let FigureEditor = class FigureEditor extends LitElement {
603
627
  const jointed = !!asset.joints?.some(j => j.body1.part === ownerOf(id));
604
628
  /* A size drag this part cannot show truly is not written: it springs back, and the reason is said. */
605
629
  if (result.scale && result.scale.some(k => Math.abs(k - 1) > 1e-4) && this.sizeDragUnsure(asset, id)) {
606
- this.error = i18next.t('figure.text.gizmo-size-turned-fastening', { part: partLabelOf(id), extreme: this.keptExtremeWord() });
630
+ this.tellSizeByFields(id);
607
631
  return this.requestUpdate();
608
632
  }
609
633
  /*
@@ -681,10 +705,6 @@ let FigureEditor = class FigureEditor extends LitElement {
681
705
  * fastened. Refused, it says why.
682
706
  */
683
707
  /* What a tilted part's typed size keeps, by the figure's placement: not the floor's lowest point everywhere (designer 2026-09-27). */
684
- keptExtremeWord() {
685
- const placement = (this.asset())?.placement ?? 'floor';
686
- return i18next.t(`figure.label.kept-extreme-${placement}`);
687
- }
688
708
  /* How the selected part would meet the plane, worked out once per version and part. */
689
709
  planeContactOf(shown, part) {
690
710
  return derivedOf(shown, `plane-contact|${part}`, () => {
@@ -886,6 +906,7 @@ let FigureEditor = class FigureEditor extends LitElement {
886
906
  renderOverlay() {
887
907
  const close = () => (this.overlay = undefined);
888
908
  const panel = (which, label, body) => html `<div
909
+ side-overlay
889
910
  bar-overlay-panel=${which}
890
911
  role="dialog"
891
912
  aria-label=${i18next.t(label)}
@@ -1458,50 +1479,46 @@ let FigureEditor = class FigureEditor extends LitElement {
1458
1479
  this.openFastening();
1459
1480
  }
1460
1481
  openFastening() {
1461
- if (!this.selected || this.fastenPopup)
1482
+ if (!this.selected || this.fastening)
1462
1483
  return;
1463
1484
  this.fastenError = '';
1464
1485
  this.fastenHover = null;
1486
+ /* One panel over the side at a time: the view or the history gives way to the window. */
1487
+ this.overlay = undefined;
1465
1488
  this.fastening = true;
1466
- const content = document.createElement('figure-fasten-dialog');
1467
- content.addEventListener('preview-command', e => this.commandPreview.ask(e.detail));
1468
- content.addEventListener('fasten-hover', e => this.hoverFastening(e.detail));
1469
- content.addEventListener('fasten-apply', e => this.applyFastening(e.detail.action));
1470
- content.addEventListener('fasten-cancel', () => this.closeFastening());
1471
- this.fastenWindow = content;
1472
- this.tellFastening();
1473
- const popup = openPopup(html `${content}`, { title: i18next.t('figure.label.fastening'), size: 'fit', backdrop: true, movable: true, closable: true, escapable: true });
1474
- this.fastenPopup = popup;
1475
- /* Closed by its own X or Esc as well: the editor lets go of it the same way. */
1476
- popup.result.then(() => this.fastenPopup === popup && this.fastenClosed());
1477
1489
  }
1478
1490
  closeFastening() {
1479
- const popup = this.fastenPopup;
1480
- this.fastenClosed();
1481
- popup?.close();
1482
- }
1483
- fastenClosed() {
1484
- this.fastenPopup = undefined;
1485
- this.fastenWindow = undefined;
1486
1491
  this.fastening = false;
1487
1492
  this.fastenError = '';
1488
1493
  this.fastenHover = null;
1489
1494
  }
1490
- /** What the window shows is the editor's: the figure on screen, the part, and what the preview and a refusal said. */
1491
- tellFastening() {
1492
- const content = this.fastenWindow;
1493
- const view = this.session?.read();
1494
- if (!content || !view)
1495
- return;
1496
- content.revision = this.shown();
1497
- content.partId = this.selected;
1498
- content.locked = view.loading || !!view.error || view.saving || !view.context;
1499
- content.previewWords = this.commandPreview.words();
1500
- content.error = this.fastenError;
1501
- }
1502
- disconnectedCallback() {
1503
- super.disconnectedCallback();
1504
- this.closeFastening();
1495
+ /**
1496
+ * The fastening window over the side panel, as the bar's panels stand there: the figure on screen, the part, and what
1497
+ * the preview and a refusal said are the editor's. Its own Esc and 「취소」 close it, as its X does.
1498
+ */
1499
+ renderFastening(unavailable) {
1500
+ if (!this.fastening || !this.selected)
1501
+ return nothing;
1502
+ return html `<div side-overlay fasten-panel>
1503
+ <div overlay-head>
1504
+ <h3>${i18next.t('figure.label.fastening')}</h3>
1505
+ <button quiet fasten-close aria-label=${i18next.t('figure.button.close')} title=${i18next.t('figure.button.close')} @click=${() => this.closeFastening()}>
1506
+ <md-icon>close</md-icon>
1507
+ </button>
1508
+ </div>
1509
+ <figure-fasten-dialog
1510
+ overlay-body
1511
+ .revision=${this.shown()}
1512
+ .partId=${this.selected}
1513
+ ?locked=${unavailable}
1514
+ .previewWords=${this.commandPreview.words()}
1515
+ .error=${this.fastenError}
1516
+ @preview-command=${(e) => this.commandPreview.ask(e.detail)}
1517
+ @fasten-hover=${(e) => this.hoverFastening(e.detail)}
1518
+ @fasten-apply=${(e) => this.applyFastening(e.detail.action)}
1519
+ @fasten-cancel=${() => this.closeFastening()}
1520
+ ></figure-fasten-dialog>
1521
+ </div>`;
1505
1522
  }
1506
1523
  /** 「적용」: the command, one step; refused, the window stays open and says why. */
1507
1524
  applyFastening(action) {
@@ -1573,7 +1590,6 @@ let FigureEditor = class FigureEditor extends LitElement {
1573
1590
  }
1574
1591
  updated(changed) {
1575
1592
  super.updated?.(changed);
1576
- this.tellFastening();
1577
1593
  /* Nothing blocks release any more: the list under the bar closes, and opens fresh when something does again. */
1578
1594
  if (changed.has('releaseBlockers') && !this.releaseBlockers.length && this.blockersOpen)
1579
1595
  this.blockersOpen = false;
@@ -1964,7 +1980,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1964
1980
  <div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
1965
1981
  </aside>
1966
1982
  </ox-three-pane>
1967
- ${this.renderOverlay()}`;
1983
+ ${this.renderOverlay()} ${this.renderFastening(unavailable)}`;
1968
1984
  }
1969
1985
  /** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
1970
1986
  renderGroups(groups, vectorTitle, singleTitle, unavailable, tools = null, held) {