@things-factory/figure-ui 10.1.224 → 10.1.226

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 (84) hide show
  1. package/client/modeller/consumer-capabilities.ts +15 -0
  2. package/client/modeller/editor-styles.ts +21 -3
  3. package/client/modeller/editor.ts +41 -27
  4. package/client/modeller/figure-capability-panel.ts +37 -10
  5. package/client/modeller/figure-clips.ts +3 -2
  6. package/client/modeller/figure-controls.ts +4 -3
  7. package/client/modeller/figure-data-panel.ts +41 -21
  8. package/client/modeller/figure-links.ts +5 -5
  9. package/client/modeller/figure-material-panel.ts +2 -0
  10. package/client/modeller/figure-preview.ts +115 -8
  11. package/client/modeller/figure-recipe-catalog.ts +4 -0
  12. package/client/modeller/figure-slot-layout-panel.ts +25 -8
  13. package/client/modeller/figure-verdict.ts +4 -1
  14. package/client/modeller/product-session.ts +3 -2
  15. package/client/modeller/unit-words.ts +11 -0
  16. package/client/pages/figure-modeller-page.ts +5 -4
  17. package/dist-client/modeller/consumer-capabilities.d.ts +11 -0
  18. package/dist-client/modeller/consumer-capabilities.js +14 -0
  19. package/dist-client/modeller/consumer-capabilities.js.map +1 -1
  20. package/dist-client/modeller/editor-styles.js +21 -3
  21. package/dist-client/modeller/editor-styles.js.map +1 -1
  22. package/dist-client/modeller/editor.js +43 -29
  23. package/dist-client/modeller/editor.js.map +1 -1
  24. package/dist-client/modeller/figure-capability-panel.d.ts +3 -0
  25. package/dist-client/modeller/figure-capability-panel.js +35 -11
  26. package/dist-client/modeller/figure-capability-panel.js.map +1 -1
  27. package/dist-client/modeller/figure-clips.js +3 -2
  28. package/dist-client/modeller/figure-clips.js.map +1 -1
  29. package/dist-client/modeller/figure-controls.js +4 -3
  30. package/dist-client/modeller/figure-controls.js.map +1 -1
  31. package/dist-client/modeller/figure-data-panel.js +42 -21
  32. package/dist-client/modeller/figure-data-panel.js.map +1 -1
  33. package/dist-client/modeller/figure-links.js +5 -5
  34. package/dist-client/modeller/figure-links.js.map +1 -1
  35. package/dist-client/modeller/figure-material-panel.js +2 -0
  36. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  37. package/dist-client/modeller/figure-preview.d.ts +3 -0
  38. package/dist-client/modeller/figure-preview.js +124 -8
  39. package/dist-client/modeller/figure-preview.js.map +1 -1
  40. package/dist-client/modeller/figure-recipe-catalog.js +4 -0
  41. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -1
  42. package/dist-client/modeller/figure-slot-layout-panel.d.ts +1 -1
  43. package/dist-client/modeller/figure-slot-layout-panel.js +25 -8
  44. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
  45. package/dist-client/modeller/figure-verdict.js +4 -1
  46. package/dist-client/modeller/figure-verdict.js.map +1 -1
  47. package/dist-client/modeller/product-session.d.ts +4 -2
  48. package/dist-client/modeller/product-session.js +3 -2
  49. package/dist-client/modeller/product-session.js.map +1 -1
  50. package/dist-client/modeller/unit-words.d.ts +5 -0
  51. package/dist-client/modeller/unit-words.js +12 -0
  52. package/dist-client/modeller/unit-words.js.map +1 -0
  53. package/dist-client/pages/figure-modeller-page.js +5 -4
  54. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  55. package/dist-client/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +10 -10
  57. package/test/ai-proposal.test.ts +2 -2
  58. package/test/browser/editing-page.js +3 -3
  59. package/test/browser/editing.check.mjs +5 -5
  60. package/test/browser/links-page.js +1 -1
  61. package/test/browser/links.check.mjs +2 -3
  62. package/test/browser/modeller-page.js +4 -4
  63. package/test/browser/modeller-revision.check.mjs +5 -5
  64. package/test/browser/modeller.check.mjs +5 -5
  65. package/test/browser/parts.check.mjs +2 -2
  66. package/test/browser/product-page.check.mjs +31 -0
  67. package/test/browser/props.check.mjs +3 -4
  68. package/test/browser/row-preview.check.mjs +3 -3
  69. package/test/browser/screen.check.mjs +13 -11
  70. package/test/browser/side-panel-sweep.check.mjs +182 -0
  71. package/test/consumer-capabilities.test.ts +7 -0
  72. package/test/figure-assistant-surface.test.ts +1 -1
  73. package/test/first-save.test.ts +2 -2
  74. package/test/links.test.ts +1 -1
  75. package/test/product-session.test.ts +5 -5
  76. package/test/proposal-record.test.ts +3 -3
  77. package/test/refusals.test.ts +1 -1
  78. package/test/revert-keeping.test.ts +1 -1
  79. package/translations/en.json +24 -12
  80. package/translations/es.json +24 -12
  81. package/translations/ja.json +25 -13
  82. package/translations/ko.json +83 -71
  83. package/translations/ms.json +24 -12
  84. package/translations/zh.json +24 -12
@@ -219,7 +219,9 @@ export class FigureMaterialPanel extends localize(i18next)(LitElement) {
219
219
  private renderMore(part: FigureAssetGeometry & { id: string }, painted: [string, string][], lit: [string, string][]) {
220
220
  const material = part.material
221
221
  const glow = material.emissive
222
+ /* Under its own title: below 「단면」 without one, transparency and glow read as the section's (sweep 2026-10-27 U21). */
222
223
  return html`<div more>
224
+ <h3>${i18next.t('figure.label.part-material')}</h3>
223
225
  <label toggle>
224
226
  <input
225
227
  type="checkbox"
@@ -2,6 +2,8 @@ import { css, html, LitElement, nothing, unsafeCSS } from 'lit'
2
2
  import { customElement, property, query, state } from 'lit/decorators.js'
3
3
 
4
4
  import { i18next, localize } from '@operato/i18n'
5
+ import { unitWord } from './unit-words.js'
6
+ import * as THREE from 'three'
5
7
  import { compileFigureAsset } from '@hatiolab/figure-model'
6
8
  import type { FigureAsset, FigureDataRow, FigureDataValue, FigureGraphStateInput } from '@hatiolab/figure-model'
7
9
  import { create, figureLegendSourcesChanged, registerFigure, unregisterFigure } from '@hatiolab/things-scene'
@@ -68,6 +70,23 @@ export class FigurePreview extends localize(i18next)(LitElement) {
68
70
  stage (the 1024 width, both side panels open) the controls lay over the help's button. One row that wraps keeps both
69
71
  whole: side by side when there is room, the controls above the help when there is not.
70
72
  */
73
+ /* A variant's name over its box: small, on the stage, never in the way of the pointer. */
74
+ div[tags] {
75
+ position: absolute;
76
+ inset: 0;
77
+ overflow: hidden;
78
+ pointer-events: none;
79
+ }
80
+ div[tags] span {
81
+ position: absolute;
82
+ transform: translate(-50%, calc(-100% - 4px));
83
+ padding: 1px 6px;
84
+ white-space: nowrap;
85
+ font-size: var(--ui-t-micro, 11px);
86
+ color: rgba(255, 255, 255, 0.92);
87
+ background-color: rgba(28, 28, 30, 0.72);
88
+ border-radius: 4px;
89
+ }
71
90
  div[foot] {
72
91
  position: absolute;
73
92
  left: 8px;
@@ -83,7 +102,26 @@ export class FigurePreview extends localize(i18next)(LitElement) {
83
102
  div[foot] > * { pointer-events: auto; }
84
103
  div[setup] { max-width: min(320px, 100%); padding: 12px; border-radius: 8px; background: rgba(28,28,30,.9); color: #fff; display: grid; gap: 8px; }
85
104
  div[setup] p { margin: 0; line-height: 1.5; }
86
- div[setup] select, div[setup] button { font: inherit; padding: 8px; min-width: 0; }
105
+ /* The help's choice and button in the look of the controls beside it, not the browser's grey (sweep 2026-10-27 U7). */
106
+ div[setup] select,
107
+ div[setup] button {
108
+ min-width: 0;
109
+ padding: 4px 8px;
110
+ font: inherit;
111
+ color: rgba(255, 255, 255, 0.92);
112
+ background-color: rgba(255, 255, 255, 0.14);
113
+ border: 1px solid rgba(255, 255, 255, 0.22);
114
+ border-radius: 5px;
115
+ }
116
+ div[setup] button {
117
+ cursor: pointer;
118
+ }
119
+ div[setup] button:hover {
120
+ background-color: rgba(255, 255, 255, 0.24);
121
+ }
122
+ div[setup] option {
123
+ color: initial;
124
+ }
87
125
  /* Which is which: side by side means nothing without a legend. */
88
126
  div[legend] {
89
127
  position: absolute;
@@ -153,22 +191,29 @@ export class FigurePreview extends localize(i18next)(LitElement) {
153
191
  margin-left: 4px;
154
192
  color: #ffcc80;
155
193
  }
194
+ /*
195
+ One grid for every row, each row its subgrid: the rows' names in one column, so a value's field starts where the one
196
+ above it does whatever the names' lengths (a row of its own grid started 28px off, sweep 2026-10-27 U7).
197
+ */
156
198
  div[clips] {
157
199
  margin-left: auto;
158
- display: flex;
159
- flex-direction: column;
160
- gap: 6px;
200
+ display: grid;
201
+ grid-template-columns: auto 20px 96px 34px;
202
+ gap: 6px 8px;
203
+ align-items: center;
161
204
  padding: 8px 10px;
162
205
  color: rgba(255, 255, 255, 0.92);
163
206
  background-color: rgba(28, 28, 30, 0.72);
164
207
  border-radius: 8px;
165
208
  font-size: var(--ui-t-micro, 11px);
166
209
  }
210
+ div[clips] > * {
211
+ grid-column: 1 / -1;
212
+ }
167
213
  div[clips] label {
168
214
  display: grid;
169
- grid-template-columns: auto 20px 96px 34px;
215
+ grid-template-columns: subgrid;
170
216
  align-items: center;
171
- gap: 8px;
172
217
  }
173
218
  /* A motion group's clip: as wide as the dial, slider and value together, so its name reads. */
174
219
  div[clips] select {
@@ -218,6 +263,20 @@ export class FigurePreview extends localize(i18next)(LitElement) {
218
263
  width: 96px;
219
264
  accent-color: var(--md-sys-color-primary);
220
265
  }
266
+ /* A value typed, not slid: in the look of the group's clip choice, starting where the sliders start. */
267
+ div[clips] input:not([type='range']) {
268
+ box-sizing: border-box;
269
+ min-width: 0;
270
+ font: inherit;
271
+ color: inherit;
272
+ background-color: rgba(255, 255, 255, 0.14);
273
+ border: 1px solid rgba(255, 255, 255, 0.22);
274
+ border-radius: 5px;
275
+ }
276
+ div[clips] input:not([type='range']):not([type='number']) {
277
+ grid-column: 3 / -1;
278
+ width: auto;
279
+ }
221
280
  div[clips] output {
222
281
  font-variant-numeric: tabular-nums;
223
282
  text-align: right;
@@ -288,6 +347,9 @@ export class FigurePreview extends localize(i18next)(LitElement) {
288
347
  }
289
348
 
290
349
  private scene?: SceneHandle
350
+ /* The eight variants named over each one, where the camera shows it (sweep 2026-10-27 M4). */
351
+ @state() private tags: { name: string; x: number; y: number }[] = []
352
+ private tagFrame = 0
291
353
  private standing?: string
292
354
  private registered?: string
293
355
  private watcher?: ResizeObserver
@@ -307,6 +369,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
307
369
  /* The part the help speaks of: the one picked in it, else the one chosen in the editor, else the first. */
308
370
  const setupPart = setupPartOf(this.setupPart, this.selected, asset?.appearance.map(p => p.target) ?? [])
309
371
  return html`<div stage></div>
372
+ ${this.tags.length ? html`<div tags>${this.tags.map(t => html`<span style=${`left:${t.x}px;top:${t.y}px`}>${t.name}</span>`)}</div>` : nothing}
310
373
  ${this.cannot
311
374
  ? html`<div cannot>${this.cannot}</div>`
312
375
  : html`<div legend>
@@ -353,10 +416,50 @@ export class FigurePreview extends localize(i18next)(LitElement) {
353
416
  </div>`
354
417
  }
355
418
 
419
+ /*
420
+ Each variant's name over its top, followed while the eight stand: the legend alone listed 「x · y · x·y …」 with
421
+ nothing saying which box was which (sweep 2026-10-27 M4).
422
+ */
423
+ private followTags() {
424
+ cancelAnimationFrame(this.tagFrame)
425
+ if (this.way !== 'eight' || !this.scene) {
426
+ if (this.tags.length) this.tags = []
427
+ return
428
+ }
429
+ const names = new Map(PREVIEW_WAYS.map(way => [`at-${way.name}`, way.axes.length ? way.name : i18next.t('figure.label.preview-base')]))
430
+ const box = new THREE.Box3()
431
+ const top = new THREE.Vector3()
432
+ const step = () => {
433
+ const root = this.scene?.root as { activeCamera3d?: THREE.Camera; renderer3d?: THREE.WebGLRenderer; components?: { model?: { id?: string }; _realObject?: { object3d?: THREE.Object3D } }[] } | undefined
434
+ const camera = root?.activeCamera3d
435
+ const canvas = root?.renderer3d?.domElement
436
+ if (!camera || !canvas || this.way !== 'eight') return
437
+ const at = canvas.getBoundingClientRect()
438
+ const host = this.getBoundingClientRect()
439
+ const tags: { name: string; x: number; y: number }[] = []
440
+ for (const component of root?.components ?? []) {
441
+ const name = names.get(component.model?.id ?? '')
442
+ const object = component._realObject?.object3d
443
+ if (!name || !object) continue
444
+ box.setFromObject(object)
445
+ if (box.isEmpty()) continue
446
+ box.getCenter(top)
447
+ top.y = box.max.y
448
+ top.project(camera)
449
+ if (top.z > 1) continue
450
+ tags.push({ name, x: Math.round((top.x * 0.5 + 0.5) * at.width + at.left - host.left), y: Math.round((-top.y * 0.5 + 0.5) * at.height + at.top - host.top) })
451
+ }
452
+ if (JSON.stringify(tags) !== JSON.stringify(this.tags)) this.tags = tags
453
+ this.tagFrame = requestAnimationFrame(step)
454
+ }
455
+ this.tagFrame = requestAnimationFrame(step)
456
+ }
457
+
356
458
  private chooseWay(way: 'one' | 'eight') {
357
459
  if (way === this.way) return
358
460
  this.way = way
359
461
  this.standUp()
462
+ this.followTags()
360
463
  /* The content is made again inside the scene standing, which keeps the camera: from one figure to eight it stood inside them. */
361
464
  requestAnimationFrame(() => this.fitView())
362
465
  }
@@ -394,7 +497,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
394
497
  const step = Math.max((control.max - control.min) / 100, 0.01)
395
498
  const label = asset.stateInputs?.[control.id]?.label ?? control.id
396
499
  return html`<label>
397
- <span>${label} <span drive>${control.unit}</span></span>
500
+ <span>${label} <span drive>${unitWord(control.unit)}</span></span>
398
501
  ${this.renderDial(control, value)}
399
502
  <input
400
503
  type="range"
@@ -454,7 +557,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
454
557
  if (node.kind === 'number') {
455
558
  const ranged = node.min !== undefined && node.max !== undefined && node.max > node.min
456
559
  return html`<label>
457
- <span>${label} <span drive>${node.unit ?? ''}</span></span>
560
+ <span>${label} <span drive>${unitWord(node.unit)}</span></span>
458
561
  <span></span>
459
562
  ${ranged
460
563
  ? html`<input type="range" min=${node.min!} max=${node.max!} step=${Math.max((node.max! - node.min!) / 100, 0.01)} .value=${String(value ?? node.min)} @input=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
@@ -639,6 +742,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
639
742
  this.cannot = ''
640
743
  this.tellLegends()
641
744
  this.tellSample()
745
+ this.followTags()
642
746
  return
643
747
  } catch {
644
748
  this.teardown()
@@ -658,12 +762,15 @@ export class FigurePreview extends localize(i18next)(LitElement) {
658
762
  this.scene.root?.fitCameraToModel?.()
659
763
  this.cannot = ''
660
764
  this.tellSample()
765
+ this.followTags()
661
766
  } catch (error) {
662
767
  this.cannot = (error as Error).message
663
768
  }
664
769
  }
665
770
 
666
771
  private teardown() {
772
+ cancelAnimationFrame(this.tagFrame)
773
+ this.tags = []
667
774
  this.scene?.dispose?.()
668
775
  this.scene = undefined
669
776
  this.standing = undefined
@@ -218,8 +218,12 @@ export class FigureRecipeCatalog extends localize(i18next)(LitElement) {
218
218
  background: var(--md-sys-color-error-container);
219
219
  border-radius: 6px;
220
220
  }
221
+ /* The guide's titles under the recipe's own (13px): they stood at 18px over it (sweep 2026-10-27 U29). */
221
222
  ox-markdown {
222
223
  display: block;
224
+ --markdown-h1-font-size: var(--ui-t-body);
225
+ --markdown-h2-font-size: var(--ui-t-body);
226
+ --markdown-h3-font-size: var(--ui-t-small);
223
227
  }
224
228
  `
225
229
  ]
@@ -3,6 +3,7 @@ import { customElement, property, state } from 'lit/decorators.js'
3
3
  import { i18next, localize } from '@operato/i18n'
4
4
  import '@material/web/icon/icon.js'
5
5
  import '@operato/slot-layout-editor'
6
+ import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
6
7
  import { compileFigureAsset, type FigureAsset, type SlotLayout } from '@hatiolab/figure-model'
7
8
 
8
9
  import {
@@ -29,15 +30,30 @@ import {
29
30
  */
30
31
  @customElement('figure-slot-layout-panel')
31
32
  export class FigureSlotLayoutPanel extends localize(i18next)(LitElement) {
32
- static styles = css`
33
+ /*
34
+ The panel's controls, and the shared editor given the same (sweep 2026-10-27 M2): its own 12px, boxed 24px fields and
35
+ right-aligned labels at 34% stood under the panel's 11.5px, 26px underlined fields and 72px labels.
36
+ */
37
+ static styles = [SidePanelStyles, css`
33
38
  :host {
34
39
  display: flex;
35
40
  flex-direction: column;
36
41
  gap: 6px;
37
42
  }
43
+ ox-slot-layout-editor {
44
+ --slot-layout-font-size: var(--ui-t-small, 11.5px);
45
+ --slot-layout-label-column: var(--figure-label-width);
46
+ --slot-layout-label-align: left;
47
+ --property-editor-field-height: var(--figure-control-height);
48
+ --slot-layout-field-border: 0 solid transparent;
49
+ --slot-layout-field-border-bottom: 1px solid var(--md-sys-color-outline-variant, #c4c7c5);
50
+ --slot-layout-field-radius: 0;
51
+ --slot-layout-field-background: transparent;
52
+ --property-editor-label-color: var(--md-sys-color-on-surface-variant);
53
+ }
38
54
  div[form] {
39
55
  display: grid;
40
- grid-template-columns: auto 1fr;
56
+ grid-template-columns: var(--figure-label-width) minmax(0, 1fr);
41
57
  gap: 4px 8px;
42
58
  align-items: center;
43
59
  }
@@ -77,7 +93,7 @@ export class FigureSlotLayoutPanel extends localize(i18next)(LitElement) {
77
93
  small[hint] {
78
94
  color: var(--md-sys-color-on-surface-variant);
79
95
  }
80
- `
96
+ `]
81
97
 
82
98
  @property({ attribute: false }) asset?: FigureAsset
83
99
  /** The part selected on the canvas: what a new anchor stands on. */
@@ -133,14 +149,14 @@ export class FigureSlotLayoutPanel extends localize(i18next)(LitElement) {
133
149
  ${(['top', 'bottom'] as const).map(f => html`<option value=${f} ?selected=${f === a.face}>${i18next.t('figure.label.face-' + f)}</option>`)}
134
150
  </select>
135
151
  <button ?disabled=${this.locked || form.anchors.length <= 1} title=${i18next.t('figure.button.remove-slot-anchor')} @click=${() => this.edit({ anchors: form.anchors.filter((_, k) => k !== i) })}>
136
- <md-icon>close</md-icon>
152
+ <md-icon>delete</md-icon>
137
153
  </button>
138
154
  </li>`
139
155
  )}
140
156
  </ul>
141
157
  <div actions>
142
158
  ${(['top', 'bottom'] as const).map(
143
- face => html`<button ?disabled=${this.locked || !this.partId} @click=${() => this.addAnchor(face)}>
159
+ face => html`<button act ?disabled=${this.locked || !this.partId} @click=${() => this.addAnchor(face)}>
144
160
  <md-icon>add</md-icon>${i18next.t(face === 'top' ? 'figure.button.add-slot-anchor-top' : 'figure.button.add-slot-anchor-bottom', { part: this.partId })}
145
161
  </button>`
146
162
  )}
@@ -151,10 +167,10 @@ export class FigureSlotLayoutPanel extends localize(i18next)(LitElement) {
151
167
 
152
168
  <div actions>
153
169
  ${stored.layout && changed
154
- ? html`<button ?disabled=${this.locked} @click=${() => this.apply(stored.layout)}><md-icon>check</md-icon>${i18next.t('figure.button.apply-slot-anchors')}</button>`
170
+ ? html`<button act ?disabled=${this.locked} @click=${() => this.apply(stored.layout)}><md-icon>check</md-icon>${i18next.t('figure.button.apply-slot-anchors')}</button>`
155
171
  : nothing}
156
172
  ${stored.layout
157
- ? html`<button ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-slot-layout' })}><md-icon>delete</md-icon>${i18next.t('figure.button.remove-slot-layout')}</button>`
173
+ ? html`<button act ?disabled=${this.locked} @click=${() => this.command({ kind: 'remove-slot-layout' })}><md-icon>delete</md-icon>${i18next.t('figure.button.remove-slot-layout')}</button>`
158
174
  : nothing}
159
175
  </div>
160
176
  `
@@ -176,7 +192,8 @@ export class FigureSlotLayoutPanel extends localize(i18next)(LitElement) {
176
192
  presetSize: startSizeOf(form.anchors, anchorFaces, anchorRoomOf(form.anchors, geometry)),
177
193
  /* No layout yet: the whole face is one slot, shown at once and put in on Apply. */
178
194
  startFrom: 'single',
179
- anchors: form.anchors.map(a => ({ id: a.id, label: `${a.id} · ${a.part} ${i18next.t('figure.label.face-' + a.face)}` })),
195
+ /* The face names its part: an anchor named as its part is not named twice (「tub · tub 윗면」, sweep 2026-10-27 M5). */
196
+ anchors: form.anchors.map(a => ({ id: a.id, label: `${a.id === a.part ? '' : `${a.id} · `}${a.part} ${i18next.t('figure.label.face-' + a.face)}` })),
180
197
  anchorFaces,
181
198
  validate: (layout: SlotLayout) => figureSlotIssues(layout, form.anchors.map(a => a.id)),
182
199
  issueText: (issue: { code: string; path: string }) =>
@@ -759,7 +759,10 @@ export class FigureVerdict extends localize(i18next)(LitElement) {
759
759
  ? html`<p quiet>${i18next.t(this.asking ? 'figure.text.verdict-asking' : 'figure.text.verdict-waiting')}</p>`
760
760
  : blocking.length
761
761
  ? html`<p>${i18next.t('figure.text.cannot-release-now')}</p>`
762
- : html`<p clear>${i18next.t('figure.text.nothing-blocks-release')}</p>`}
762
+ : this.revision && !this.revision.asset.appearance.length
763
+ ? /* No part: nothing to judge, not 「nothing blocks」 (sweep 2026-10-27 U24). */
764
+ html`<p quiet>${i18next.t('figure.text.release-needs-a-part')}</p>`
765
+ : html`<p clear>${i18next.t('figure.text.nothing-blocks-release')}</p>`}
763
766
  ${blocking.length
764
767
  ? html`<ul gate>
765
768
  <li head>${i18next.t('figure.text.must-fix-to-release')}</li>
@@ -162,12 +162,13 @@ export function createFigureProductSession(store: FigureProductStore) {
162
162
  * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.
163
163
  */
164
164
  confirmedSource: () => (id ? baseline : undefined),
165
- startNew({ discard = false } = {}) {
165
+ /** A new figure, named in the screen's language by the page: the session has no words of its own. */
166
+ startNew({ discard = false, name }: { discard?: boolean; name: string }) {
166
167
  canLeave(discard)
167
168
  ++epoch
168
169
  clear()
169
170
  loading = false
170
- workspace.open(serializeFigureAsset(createFigureAsset('새 Figure')))
171
+ workspace.open(serializeFigureAsset(createFigureAsset(name)))
171
172
  creationId = crypto.randomUUID()
172
173
  return read()
173
174
  },
@@ -0,0 +1,11 @@
1
+ import { i18next } from '@operato/i18n'
2
+
3
+ /**
4
+ * A unit as the screen names it: the graph's codes (deg, percent, count …) are not words a person reads, and a unit
5
+ * typed by the author (rpm) stays as typed (sweep 2026-10-27 U9).
6
+ */
7
+ export function unitWord(unit: string | undefined): string {
8
+ if (!unit) return ''
9
+ const key = `figure.label.unit-${unit}`
10
+ return i18next.exists(key) ? i18next.t(key) : unit
11
+ }
@@ -450,7 +450,7 @@ export class FigureModellerPage extends PageView {
450
450
  await opening
451
451
  this.readRow(id)
452
452
  } else {
453
- this.session.startNew({ discard })
453
+ this.session.startNew({ discard, name: i18next.t('figure.text.new-figure-name') })
454
454
  this.row = { version: 0 }
455
455
  this.typeError = ''
456
456
  this.takeNewFigureProposal()
@@ -496,12 +496,13 @@ export class FigureModellerPage extends PageView {
496
496
  get context() {
497
497
  const view = this.session.read()
498
498
  return {
499
- title: view.name ?? 'Figure',
499
+ title: view.name || i18next.t('figure.label.figure'),
500
500
  /*
501
501
  A figure that has an id was opened from the catalog (or saved there), so the rail marks the catalog. Without this
502
- the rail matches the page name and marks "new figure" while an existing one is being edited.
502
+ the rail matches the page name and marks "new figure" while an existing one is being edited. The address names
503
+ the figure before it is read: opened by its address, the rail marked "new figure" (sweep 2026-10-27 U30).
503
504
  */
504
- menu: view.id ? 'figure-list' : undefined,
505
+ menu: view.id || this.lifecycle?.resourceId ? 'figure-list' : undefined,
505
506
  /* The canvas needs the width: the rail folds while the modeller is open and comes back as it was on leaving. */
506
507
  rail: 'collapsed',
507
508
  actions: [],
@@ -1,11 +1,22 @@
1
1
  import type { FigureAsset } from '@hatiolab/figure-model';
2
2
  import { type FigureCapabilityDescriptor, type FigureCapabilitySetting } from '@hatiolab/things-scene';
3
+ import { type Rung } from '@things-factory/figure-viewer/dist-client/capability-rung.js';
3
4
  /** The products' capabilities this modeller knows: registered descriptors with a namespaced name. */
4
5
  export declare const consumerDescriptors: () => FigureCapabilityDescriptor[];
5
6
  /** The products' capabilities the figure declares that this modeller has no descriptor for: kept, said, removable. */
6
7
  export declare function unknownConsumerNames(asset: FigureAsset): string[];
7
8
  /** What a capability still needs on this figure: a shared or a product's name not declared, or the item mark. */
8
9
  export declare function needsMissing(descriptor: FigureCapabilityDescriptor, asset: FigureAsset): string[];
10
+ /**
11
+ * What a product's capability needs that the figure lacks, as the tab offers it: the item tag, and the rungs that would
12
+ * give the names it needs (Holdable | FlowNode and Capacity | FlowNode: hold or flow). Its own names are not shown to the
13
+ * author (sweep 2026-10-27 U19); `names` keeps a need no rung gives.
14
+ */
15
+ export declare function needsOffered(descriptor: FigureCapabilityDescriptor, asset: FigureAsset): {
16
+ item: boolean;
17
+ rungs: Rung[];
18
+ names: string[];
19
+ };
9
20
  /** The names with a product's capability put on or taken off; the shared ones as they are. */
10
21
  export declare function namesWith(asset: FigureAsset, name: string, on: boolean): string[];
11
22
  /** A capability's settings on the asset, or an empty object. */
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { isFigureConsumerCapability } from '@hatiolab/figure-model';
8
8
  import { figureCapabilityDescriptors } from '@hatiolab/things-scene';
9
+ import { RUNGS } from '@things-factory/figure-viewer/dist-client/capability-rung.js';
9
10
  /** The products' capabilities this modeller knows: registered descriptors with a namespaced name. */
10
11
  export const consumerDescriptors = () => figureCapabilityDescriptors().filter(d => isFigureConsumerCapability(d.name));
11
12
  /** The products' capabilities the figure declares that this modeller has no descriptor for: kept, said, removable. */
@@ -19,6 +20,19 @@ export function needsMissing(descriptor, asset) {
19
20
  const has = (need) => (need === 'item' ? !!asset.item : names.has(need));
20
21
  return descriptor.needs.filter(need => (Array.isArray(need) ? !need.some(has) : !has(need))).map(need => (Array.isArray(need) ? need.join(' | ') : need));
21
22
  }
23
+ /**
24
+ * What a product's capability needs that the figure lacks, as the tab offers it: the item tag, and the rungs that would
25
+ * give the names it needs (Holdable | FlowNode and Capacity | FlowNode: hold or flow). Its own names are not shown to the
26
+ * author (sweep 2026-10-27 U19); `names` keeps a need no rung gives.
27
+ */
28
+ export function needsOffered(descriptor, asset) {
29
+ const missing = needsMissing(descriptor, asset);
30
+ const groups = descriptor.needs.filter(need => need !== 'item').map(need => (Array.isArray(need) ? need : [need]));
31
+ const gives = (rung) => groups.every(group => group.some(name => RUNGS[rung].includes(name)));
32
+ const lacking = missing.filter(m => m !== 'item');
33
+ const rungs = lacking.length ? ['hold', 'flow'].filter(gives) : [];
34
+ return { item: missing.includes('item'), rungs, names: lacking.length && !rungs.length ? lacking : [] };
35
+ }
22
36
  /** The names with a product's capability put on or taken off; the shared ones as they are. */
23
37
  export function namesWith(asset, name, on) {
24
38
  const names = asset.document.capabilities.names ?? [];
@@ -1 +1 @@
1
- {"version":3,"file":"consumer-capabilities.js","sourceRoot":"","sources":["../../client/modeller/consumer-capabilities.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A product's capabilities in the modeller (ADR-0167 3, 9, 12): the descriptors registered with the scene, which ones\n * the figure wears, what each still needs, and its settings on the asset as its descriptor draws them -- parts picked,\n * an input and a value per fixed key. The descriptor's own check says what the settings lack. Kept apart from the\n * element so it is tested without a browser.\n */\nimport { isFigureConsumerCapability } from '@hatiolab/figure-model'\nimport type { FigureAsset } from '@hatiolab/figure-model'\nimport { figureCapabilityDescriptors, type FigureCapabilityDescriptor, type FigureCapabilitySetting } from '@hatiolab/things-scene'\n\n/** The products' capabilities this modeller knows: registered descriptors with a namespaced name. */\nexport const consumerDescriptors = (): FigureCapabilityDescriptor[] => figureCapabilityDescriptors().filter(d => isFigureConsumerCapability(d.name))\n\n/** The products' capabilities the figure declares that this modeller has no descriptor for: kept, said, removable. */\nexport function unknownConsumerNames(asset: FigureAsset): string[] {\n const known = new Set(consumerDescriptors().map(d => d.name))\n return (asset.document.capabilities.names ?? []).filter(n => isFigureConsumerCapability(n) && !known.has(n))\n}\n\n/** What a capability still needs on this figure: a shared or a product's name not declared, or the item mark. */\nexport function needsMissing(descriptor: FigureCapabilityDescriptor, asset: FigureAsset): string[] {\n const names = new Set(asset.document.capabilities.names ?? [])\n const has = (need: string) => (need === 'item' ? !!asset.item : names.has(need))\n return descriptor.needs.filter(need => (Array.isArray(need) ? !need.some(has) : !has(need as string))).map(need => (Array.isArray(need) ? need.join(' | ') : (need as string)))\n}\n\n/** The names with a product's capability put on or taken off; the shared ones as they are. */\nexport function namesWith(asset: FigureAsset, name: string, on: boolean): string[] {\n const names = asset.document.capabilities.names ?? []\n return on ? [...new Set([...names, name])] : names.filter(n => n !== name)\n}\n\n/** A capability's settings on the asset, or an empty object. */\nexport const settingsOf = (asset: FigureAsset, name: string): Record<string, unknown> => ({ ...(asset.document.capabilities.extensions?.[name] ?? {}) })\n\n/** The parts a parts setting holds. */\nexport function partsAt(settings: Record<string, unknown>, key: string): string[] {\n const v = settings[key]\n return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []\n}\n\n/** The settings with a parts setting changed; none is left out. */\nexport function withParts(settings: Record<string, unknown>, key: string, parts: string[]): Record<string, unknown> {\n const next = { ...settings }\n if (parts.length) next[key] = [...parts]\n else delete next[key]\n return next\n}\n\n/** An input table as held: the input, and a value per fixed key (the descriptor's start where none is given yet). */\nexport function inputTableAt(settings: Record<string, unknown>, setting: Extract<FigureCapabilitySetting, { kind: 'input-table' }>): { input?: string; values: Record<string, number> } {\n const v = settings[setting.key] as { input?: unknown; values?: Record<string, unknown> } | undefined\n const values = Object.fromEntries(setting.keys.map(k => [k, typeof v?.values?.[k] === 'number' ? (v.values[k] as number) : setting.start[k]!]))\n return { ...(typeof v?.input === 'string' ? { input: v.input } : {}), values }\n}\n\n/** The settings with an input table changed: an input chosen writes every fixed key's value; none takes the table away. */\nexport function withInputTable(settings: Record<string, unknown>, setting: Extract<FigureCapabilitySetting, { kind: 'input-table' }>, input: string | undefined, values: Record<string, number>): Record<string, unknown> {\n const next = { ...settings }\n if (input) next[setting.key] = { input, values: Object.fromEntries(setting.keys.map(k => [k, values[k] ?? setting.start[k]!])) }\n else delete next[setting.key]\n return next\n}\n\n/** What the capability's own check says of the settings now; nothing when it has no check. */\nexport const settingsIssues = (descriptor: FigureCapabilityDescriptor, asset: FigureAsset): string[] => descriptor.check?.(asset.document.capabilities.extensions?.[descriptor.name], asset) ?? []\n"]}
1
+ {"version":3,"file":"consumer-capabilities.js","sourceRoot":"","sources":["../../client/modeller/consumer-capabilities.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A product's capabilities in the modeller (ADR-0167 3, 9, 12): the descriptors registered with the scene, which ones\n * the figure wears, what each still needs, and its settings on the asset as its descriptor draws them -- parts picked,\n * an input and a value per fixed key. The descriptor's own check says what the settings lack. Kept apart from the\n * element so it is tested without a browser.\n */\nimport { isFigureConsumerCapability } from '@hatiolab/figure-model'\nimport type { FigureAsset } from '@hatiolab/figure-model'\nimport { figureCapabilityDescriptors, type FigureCapabilityDescriptor, type FigureCapabilitySetting } from '@hatiolab/things-scene'\nimport { RUNGS, type Rung } from '@things-factory/figure-viewer/dist-client/capability-rung.js'\n\n/** The products' capabilities this modeller knows: registered descriptors with a namespaced name. */\nexport const consumerDescriptors = (): FigureCapabilityDescriptor[] => figureCapabilityDescriptors().filter(d => isFigureConsumerCapability(d.name))\n\n/** The products' capabilities the figure declares that this modeller has no descriptor for: kept, said, removable. */\nexport function unknownConsumerNames(asset: FigureAsset): string[] {\n const known = new Set(consumerDescriptors().map(d => d.name))\n return (asset.document.capabilities.names ?? []).filter(n => isFigureConsumerCapability(n) && !known.has(n))\n}\n\n/** What a capability still needs on this figure: a shared or a product's name not declared, or the item mark. */\nexport function needsMissing(descriptor: FigureCapabilityDescriptor, asset: FigureAsset): string[] {\n const names = new Set(asset.document.capabilities.names ?? [])\n const has = (need: string) => (need === 'item' ? !!asset.item : names.has(need))\n return descriptor.needs.filter(need => (Array.isArray(need) ? !need.some(has) : !has(need as string))).map(need => (Array.isArray(need) ? need.join(' | ') : (need as string)))\n}\n\n/**\n * What a product's capability needs that the figure lacks, as the tab offers it: the item tag, and the rungs that would\n * give the names it needs (Holdable | FlowNode and Capacity | FlowNode: hold or flow). Its own names are not shown to the\n * author (sweep 2026-10-27 U19); `names` keeps a need no rung gives.\n */\nexport function needsOffered(descriptor: FigureCapabilityDescriptor, asset: FigureAsset): { item: boolean; rungs: Rung[]; names: string[] } {\n const missing = needsMissing(descriptor, asset)\n const groups = descriptor.needs.filter(need => need !== 'item').map(need => (Array.isArray(need) ? need : [need as string]))\n const gives = (rung: Rung) => groups.every(group => group.some(name => (RUNGS[rung] as readonly string[]).includes(name)))\n const lacking = missing.filter(m => m !== 'item')\n const rungs = lacking.length ? (['hold', 'flow'] as const).filter(gives) : []\n return { item: missing.includes('item'), rungs, names: lacking.length && !rungs.length ? lacking : [] }\n}\n\n/** The names with a product's capability put on or taken off; the shared ones as they are. */\nexport function namesWith(asset: FigureAsset, name: string, on: boolean): string[] {\n const names = asset.document.capabilities.names ?? []\n return on ? [...new Set([...names, name])] : names.filter(n => n !== name)\n}\n\n/** A capability's settings on the asset, or an empty object. */\nexport const settingsOf = (asset: FigureAsset, name: string): Record<string, unknown> => ({ ...(asset.document.capabilities.extensions?.[name] ?? {}) })\n\n/** The parts a parts setting holds. */\nexport function partsAt(settings: Record<string, unknown>, key: string): string[] {\n const v = settings[key]\n return Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []\n}\n\n/** The settings with a parts setting changed; none is left out. */\nexport function withParts(settings: Record<string, unknown>, key: string, parts: string[]): Record<string, unknown> {\n const next = { ...settings }\n if (parts.length) next[key] = [...parts]\n else delete next[key]\n return next\n}\n\n/** An input table as held: the input, and a value per fixed key (the descriptor's start where none is given yet). */\nexport function inputTableAt(settings: Record<string, unknown>, setting: Extract<FigureCapabilitySetting, { kind: 'input-table' }>): { input?: string; values: Record<string, number> } {\n const v = settings[setting.key] as { input?: unknown; values?: Record<string, unknown> } | undefined\n const values = Object.fromEntries(setting.keys.map(k => [k, typeof v?.values?.[k] === 'number' ? (v.values[k] as number) : setting.start[k]!]))\n return { ...(typeof v?.input === 'string' ? { input: v.input } : {}), values }\n}\n\n/** The settings with an input table changed: an input chosen writes every fixed key's value; none takes the table away. */\nexport function withInputTable(settings: Record<string, unknown>, setting: Extract<FigureCapabilitySetting, { kind: 'input-table' }>, input: string | undefined, values: Record<string, number>): Record<string, unknown> {\n const next = { ...settings }\n if (input) next[setting.key] = { input, values: Object.fromEntries(setting.keys.map(k => [k, values[k] ?? setting.start[k]!])) }\n else delete next[setting.key]\n return next\n}\n\n/** What the capability's own check says of the settings now; nothing when it has no check. */\nexport const settingsIssues = (descriptor: FigureCapabilityDescriptor, asset: FigureAsset): string[] => descriptor.check?.(asset.document.capabilities.extensions?.[descriptor.name], asset) ?? []\n"]}
@@ -18,7 +18,8 @@ export const EditorStyles = css `
18
18
  /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and
19
19
  fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */
20
20
  ox-canvas-bar button[mode] {
21
- height: 28px;
21
+ /* The bar's height, beside its 32px icons and the save (sweep 2026-10-27 U25). */
22
+ height: 32px;
22
23
  border: 1px solid transparent;
23
24
  padding: 0 10px;
24
25
  border-radius: 999em;
@@ -206,6 +207,22 @@ export const EditorStyles = css `
206
207
  margin-top: 4px;
207
208
  color: var(--md-sys-color-on-surface-variant);
208
209
  }
210
+ div[start] button[start-more] {
211
+ display: block;
212
+ text-align: left;
213
+ margin: 4px 0 0;
214
+ padding: 0;
215
+ font: inherit;
216
+ font-size: var(--ui-t-small);
217
+ color: var(--md-sys-color-primary);
218
+ background: none;
219
+ border: none;
220
+ cursor: pointer;
221
+ }
222
+ /* No rule under the last way to start: it closed nothing (sweep 2026-10-27 U23). */
223
+ div[start-paths] > section:last-of-type {
224
+ border-bottom: none;
225
+ }
209
226
  /* The side: tabs over one panel at a time, each using the whole height. */
210
227
  /*
211
228
  The side's words at the panel's size (side-panel-styles: titles body, the rest small). The editor's host is at body
@@ -338,11 +355,12 @@ export const EditorStyles = css `
338
355
  min-height: 0;
339
356
  overflow: auto;
340
357
  }
341
- /* Which part the panel speaks of. */
358
+ /* Which part the panel speaks of. Above z-index 1: a chip's label (md-filter-chip) sits at 1 and, coming later, was
359
+ drawn over it when the panel scrolled (sweep 2026-10-27 U2). */
342
360
  div[subject] {
343
361
  position: sticky;
344
362
  top: 0;
345
- z-index: 1;
363
+ z-index: 2;
346
364
  display: grid;
347
365
  gap: 4px;
348
366
  padding: 10px 12px;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4a9B,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 /*\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 /* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */\n p[same-measure] {\n position: absolute;\n bottom: 12px;\n left: 50%;\n z-index: 5;\n transform: translateX(-50%);\n margin: 0;\n padding: 4px 10px;\n border-radius: 999em;\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-small, 11.5px);\n pointer-events: none;\n white-space: nowrap;\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 /* A range's two ends read as one: smallest – largest. */\n div[pair][range] {\n align-items: center;\n }\n div[pair][range] > span[dash] {\n flex: none;\n color: var(--md-sys-color-on-surface-variant);\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 /* 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8b9B,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 /* The bar's height, beside its 32px icons and the save (sweep 2026-10-27 U25). */\n height: 32px;\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 /*\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 /* Which size handles change one diameter (ADR-0140 4.1): a quiet line over the canvas, out of the gizmo's way. */\n p[same-measure] {\n position: absolute;\n bottom: 12px;\n left: 50%;\n z-index: 5;\n transform: translateX(-50%);\n margin: 0;\n padding: 4px 10px;\n border-radius: 999em;\n background: var(--md-sys-color-surface-container-high);\n color: var(--md-sys-color-on-surface-variant);\n font-size: var(--ui-t-small, 11.5px);\n pointer-events: none;\n white-space: nowrap;\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 div[start] button[start-more] {\n display: block;\n text-align: left;\n margin: 4px 0 0;\n padding: 0;\n font: inherit;\n font-size: var(--ui-t-small);\n color: var(--md-sys-color-primary);\n background: none;\n border: none;\n cursor: pointer;\n }\n /* No rule under the last way to start: it closed nothing (sweep 2026-10-27 U23). */\n div[start-paths] > section:last-of-type {\n border-bottom: none;\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 /* A range's two ends read as one: smallest – largest. */\n div[pair][range] {\n align-items: center;\n }\n div[pair][range] > span[dash] {\n flex: none;\n color: var(--md-sys-color-on-surface-variant);\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 /* Which part the panel speaks of. Above z-index 1: a chip's label (md-filter-chip) sits at 1 and, coming later, was\n drawn over it when the panel scrolled (sweep 2026-10-27 U2). */\n div[subject] {\n position: sticky;\n top: 0;\n z-index: 2;\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"]}