@things-factory/figure-ui 10.1.168 → 10.1.170

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 (123) hide show
  1. package/client/figure-assistant.ts +10 -1
  2. package/client/modeller/affected-parts.ts +11 -0
  3. package/client/modeller/ai-proposal.ts +40 -9
  4. package/client/modeller/canvas.ts +15 -1
  5. package/client/modeller/editor-styles.ts +98 -6
  6. package/client/modeller/editor.ts +212 -53
  7. package/client/modeller/fasten-geometry.ts +19 -0
  8. package/client/modeller/figure-fasten-drawing.ts +4 -3
  9. package/client/modeller/figure-links.ts +80 -37
  10. package/client/modeller/figure-material-panel.ts +20 -1
  11. package/client/modeller/figure-preview.ts +117 -8
  12. package/client/modeller/figure-properties.ts +1 -2
  13. package/client/modeller/figure-section-panel.ts +5 -0
  14. package/client/modeller/figure-verdict.ts +9 -2
  15. package/client/modeller/gate-findings.ts +12 -4
  16. package/client/modeller/group-menu.ts +57 -0
  17. package/client/modeller/links.ts +22 -1
  18. package/client/modeller/modeller-keys.ts +6 -0
  19. package/client/modeller/preview-size.ts +32 -0
  20. package/client/modeller/product-session.ts +23 -9
  21. package/client/modeller/refusals.ts +14 -0
  22. package/client/modeller/stage-model.ts +11 -0
  23. package/client/pages/figure-modeller-page.ts +15 -6
  24. package/dist-client/figure-assistant.d.ts +6 -0
  25. package/dist-client/figure-assistant.js +9 -1
  26. package/dist-client/figure-assistant.js.map +1 -1
  27. package/dist-client/modeller/affected-parts.d.ts +6 -0
  28. package/dist-client/modeller/affected-parts.js +12 -0
  29. package/dist-client/modeller/affected-parts.js.map +1 -0
  30. package/dist-client/modeller/ai-proposal.d.ts +15 -5
  31. package/dist-client/modeller/ai-proposal.js +39 -10
  32. package/dist-client/modeller/ai-proposal.js.map +1 -1
  33. package/dist-client/modeller/canvas.d.ts +3 -0
  34. package/dist-client/modeller/canvas.js +12 -1
  35. package/dist-client/modeller/canvas.js.map +1 -1
  36. package/dist-client/modeller/editor-styles.js +98 -6
  37. package/dist-client/modeller/editor-styles.js.map +1 -1
  38. package/dist-client/modeller/editor.d.ts +42 -1
  39. package/dist-client/modeller/editor.js +220 -51
  40. package/dist-client/modeller/editor.js.map +1 -1
  41. package/dist-client/modeller/fasten-geometry.d.ts +27 -0
  42. package/dist-client/modeller/fasten-geometry.js +14 -0
  43. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  44. package/dist-client/modeller/figure-fasten-drawing.js +4 -3
  45. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  46. package/dist-client/modeller/figure-links.d.ts +3 -1
  47. package/dist-client/modeller/figure-links.js +84 -38
  48. package/dist-client/modeller/figure-links.js.map +1 -1
  49. package/dist-client/modeller/figure-material-panel.d.ts +8 -1
  50. package/dist-client/modeller/figure-material-panel.js +24 -1
  51. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  52. package/dist-client/modeller/figure-preview.d.ts +12 -0
  53. package/dist-client/modeller/figure-preview.js +116 -5
  54. package/dist-client/modeller/figure-preview.js.map +1 -1
  55. package/dist-client/modeller/figure-properties.js +1 -2
  56. package/dist-client/modeller/figure-properties.js.map +1 -1
  57. package/dist-client/modeller/figure-section-panel.d.ts +1 -1
  58. package/dist-client/modeller/figure-section-panel.js +5 -0
  59. package/dist-client/modeller/figure-section-panel.js.map +1 -1
  60. package/dist-client/modeller/figure-verdict.d.ts +1 -0
  61. package/dist-client/modeller/figure-verdict.js +8 -2
  62. package/dist-client/modeller/figure-verdict.js.map +1 -1
  63. package/dist-client/modeller/gate-findings.d.ts +3 -1
  64. package/dist-client/modeller/gate-findings.js +8 -4
  65. package/dist-client/modeller/gate-findings.js.map +1 -1
  66. package/dist-client/modeller/group-menu.d.ts +18 -0
  67. package/dist-client/modeller/group-menu.js +52 -0
  68. package/dist-client/modeller/group-menu.js.map +1 -0
  69. package/dist-client/modeller/links.d.ts +5 -0
  70. package/dist-client/modeller/links.js +21 -1
  71. package/dist-client/modeller/links.js.map +1 -1
  72. package/dist-client/modeller/modeller-keys.js +7 -0
  73. package/dist-client/modeller/modeller-keys.js.map +1 -1
  74. package/dist-client/modeller/preview-size.d.ts +23 -0
  75. package/dist-client/modeller/preview-size.js +32 -0
  76. package/dist-client/modeller/preview-size.js.map +1 -0
  77. package/dist-client/modeller/product-session.d.ts +0 -1
  78. package/dist-client/modeller/product-session.js +22 -9
  79. package/dist-client/modeller/product-session.js.map +1 -1
  80. package/dist-client/modeller/refusals.d.ts +1 -5
  81. package/dist-client/modeller/refusals.js +14 -1
  82. package/dist-client/modeller/refusals.js.map +1 -1
  83. package/dist-client/modeller/stage-model.d.ts +11 -0
  84. package/dist-client/modeller/stage-model.js +11 -0
  85. package/dist-client/modeller/stage-model.js.map +1 -1
  86. package/dist-client/pages/figure-modeller-page.d.ts +3 -0
  87. package/dist-client/pages/figure-modeller-page.js +18 -5
  88. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  89. package/dist-client/route.d.ts +1 -1
  90. package/dist-client/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +8 -8
  92. package/test/ai-proposal.test.ts +43 -4
  93. package/test/browser/editing-page.js +3 -1
  94. package/test/browser/editing.check.mjs +2 -1
  95. package/test/browser/links-page.js +1 -0
  96. package/test/browser/links.check.mjs +20 -5
  97. package/test/browser/modeller.check.mjs +6 -0
  98. package/test/browser/more-open.js +11 -0
  99. package/test/browser/parts.check.mjs +11 -1
  100. package/test/browser/places-mesh.check.mjs +9 -3
  101. package/test/browser/product-page.check.mjs +9 -4
  102. package/test/browser/props.check.mjs +224 -0
  103. package/test/browser/screen-page.js +17 -0
  104. package/test/browser/screen.check.mjs +171 -29
  105. package/test/browser/section-page.js +1 -0
  106. package/test/browser/section.check.mjs +6 -0
  107. package/test/command-preview.test.ts +2 -2
  108. package/test/fasten-geometry.test.ts +15 -0
  109. package/test/fields.test.ts +4 -2
  110. package/test/figure-assistant-surface.test.ts +38 -0
  111. package/test/first-minute.test.ts +15 -2
  112. package/test/layout.test.ts +1 -1
  113. package/test/preview-size.test.ts +33 -0
  114. package/test/refusals.test.ts +1 -1
  115. package/test/resize-with-placement.test.ts +1 -1
  116. package/test/stage-model.test.ts +11 -1
  117. package/test/verdict.test.ts +7 -0
  118. package/translations/en.json +43 -27
  119. package/translations/es.json +24 -8
  120. package/translations/ja.json +25 -9
  121. package/translations/ko.json +56 -40
  122. package/translations/ms.json +25 -9
  123. package/translations/zh.json +25 -9
@@ -12,10 +12,19 @@ import { registerProposalHandler } from '@things-factory/ai-assistant/dist-clien
12
12
 
13
13
  import { fetchProposalOutcome } from './graphql/index.js'
14
14
  import { modellerPathOf } from './modeller/modeller-route.js'
15
- import { pendNewFigure, runFigureProposal, type FigureApplyDetail } from './modeller/ai-proposal.js'
15
+ import { pendNewFigure, proposalOutcomeFrom, runFigureProposal, type FigureApplyDetail } from './modeller/ai-proposal.js'
16
16
 
17
17
  export const FIGURE_CANDIDATE_TOOL = 'proposeFigureCandidate'
18
18
 
19
+ /**
20
+ * The dock's "check the result" of a figure proposal, on every screen that offers its card: what the server recorded of
21
+ * its save (ADR-0097 §5.6 item 5). The modeller asks it, and so does an app's home where a new figure's card is made --
22
+ * there it was missing, and a card cut off by a reload stayed "no answer" for good (figure audit 2026-10-16 N8).
23
+ */
24
+ export async function figureProposalOutcome(proposalKey: string) {
25
+ return proposalOutcomeFrom(await fetchProposalOutcome(proposalKey), (key, params) => i18next.t(key, params))
26
+ }
27
+
19
28
  registerProposalHandler(FIGURE_CANDIDATE_TOOL, async (proposal, context) => {
20
29
  const outcome = await runFigureProposal(
21
30
  proposal,
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The parts a release finding points at, by the author's names. The server's `at` lists the first word of each path it
3
+ * could not prove -- a part's name, or a section of the format such as 「occupancy」 -- and the screen showed it as it came
4
+ * (「occupancy · roller-row-1-roller」, user 2026-10-16). Only the figure's own part names are kept.
5
+ */
6
+ export function affectedPartsOf(at: string, parts: readonly string[]): string[] {
7
+ return at
8
+ .split('·')
9
+ .map(word => word.trim())
10
+ .filter(word => parts.includes(word))
11
+ }
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { FigureProposalStep } from '@hatiolab/figure-model'
9
9
  import type { FigureProductSession } from './product-session.js'
10
- import { clipLossOf, refusalRecordOf, type FigureRefusalRecord } from './refusals.js'
10
+ import { clipLossOf, editRefusalOf, refusalRecordOf, type FigureRefusalRecord } from './refusals.js'
11
11
 
12
12
  /** What the card carries, as `proposeFigureCandidate` returned it. */
13
13
  export interface FigureProposalCard {
@@ -28,6 +28,8 @@ export interface FigureApplyDetail extends FigureRefusalRecord {
28
28
  key?: string
29
29
  steps: FigureProposalStep[]
30
30
  create: boolean
31
+ /** The figure a revision was made for: the one the model read. */
32
+ figureId?: string
31
33
  context?: string
32
34
  revision?: number
33
35
  applied?: boolean
@@ -82,7 +84,8 @@ function refused(refusal: FigureRefusalRecord & { error?: string }, say: Say): F
82
84
  ? (error ?? '')
83
85
  : clips
84
86
  ? say('figure.text.proposal-would-drop-clips', { clips: clips.map(c => c.clip).join(', ') })
85
- : say('figure.text.proposal-refused', { reason: error ?? '' })
87
+ : /* A refusal with a code is said from it, as the modeller says an edit it refuses; never its message (audit N6). */
88
+ say('figure.text.proposal-refused', { reason: code ? editRefusalOf(refusal, (key, params) => say(key, params)) : (error ?? '') })
86
89
  return { verdict: 'refused', errorCode: code ?? 'refused', note }
87
90
  }
88
91
 
@@ -99,6 +102,12 @@ export function takeFigureOffer(
99
102
  ): boolean {
100
103
  const view = session.read()
101
104
  if (detail.create && onSavedFigure) return false
105
+ /*
106
+ A revision is for the figure the model read; with another open it is left untaken, and the handler says it needs that
107
+ figure open. The version is no proof of the figure: two figures opened in two page loads stood at the same one
108
+ (figure audit 2026-10-16 N3).
109
+ */
110
+ if (!detail.create && typeof detail.figureId === 'string' && detail.figureId !== (view.id ?? view.creationId)) return false
102
111
  if (detail.create && view.dirty) {
103
112
  /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */
104
113
  detail.applied = false
@@ -128,7 +137,7 @@ export function takeFigureOffer(
128
137
 
129
138
  export async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {
130
139
  const steps = card?.steps
131
- if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }
140
+ if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps', note: say('figure.text.proposal-empty') }
132
141
  const create = card.create === true
133
142
  /*
134
143
  A new figure's proposal already saved into a figure is not made again: that figure is opened (ADR-0061 decision 3,
@@ -147,7 +156,7 @@ export async function runFigureProposal(card: FigureProposalCard, target: Figure
147
156
  note: say('figure.text.proposal-applied', { n: String(steps.length) })
148
157
  })
149
158
 
150
- const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision })
159
+ const detail = target.offer({ key: card.key, steps, create, figureId: create ? undefined : card.figureId, context: card.context, revision: card.revision })
151
160
  if (detail.applied) return accepted()
152
161
  if (detail.applied === false) return refused(detail, say)
153
162
 
@@ -166,11 +175,14 @@ export async function runFigureProposal(card: FigureProposalCard, target: Figure
166
175
  modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new
167
176
  figure must not receive steps nobody is waiting for.
168
177
  */
169
- let pending: { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined
178
+ type Settle = (result: { applied: boolean; error?: string; code?: string }) => void
179
+ let pending: { steps: FigureProposalStep[]; key?: string; settle: Settle; hold(): void; resume(): void } | undefined
170
180
 
171
181
  export function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {
172
182
  pending?.settle({ applied: false, code: 'not-opened' })
173
183
  return new Promise(resolve => {
184
+ let timer: ReturnType<typeof setTimeout> | undefined
185
+ const lapse = () => mine.settle({ applied: false, code: 'not-opened' })
174
186
  const mine = {
175
187
  steps,
176
188
  key,
@@ -178,17 +190,36 @@ export function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10
178
190
  clearTimeout(timer)
179
191
  if (pending === mine) pending = undefined
180
192
  resolve(result)
193
+ },
194
+ hold: () => clearTimeout(timer),
195
+ resume: () => {
196
+ clearTimeout(timer)
197
+ timer = setTimeout(lapse, ms)
181
198
  }
182
199
  }
183
- const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)
200
+ mine.resume()
184
201
  pending = mine
185
202
  })
186
203
  }
187
204
 
205
+ /**
206
+ * For a page about to ask the person whether to leave unsaved edits: the new figure waiting to open stops its clock
207
+ * while they answer -- an answer is no absent page (figure audit 2026-10-16 N4). Give it the answer: leaving runs the
208
+ * clock again for the page that opens; staying says now that it was not opened. Nothing waiting: undefined.
209
+ */
210
+ export function askingToLeave(): ((leaving: boolean) => void) | undefined {
211
+ const waiting = pending
212
+ if (!waiting) return undefined
213
+ waiting.hold()
214
+ return leaving => {
215
+ if (pending !== waiting) return
216
+ if (leaving) waiting.resume()
217
+ else waiting.settle({ applied: false, code: 'not-opened' })
218
+ }
219
+ }
220
+
188
221
  /** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */
189
- export function takePendingNewFigure():
190
- | { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void }
191
- | undefined {
222
+ export function takePendingNewFigure(): { steps: FigureProposalStep[]; key?: string; settle: Settle } | undefined {
192
223
  const taken = pending
193
224
  pending = undefined
194
225
  return taken
@@ -5,7 +5,7 @@ import { create, Model, niceGridStep } from '@hatiolab/things-scene'
5
5
  import type { GizmoReach } from './gizmo.js'
6
6
  import type { FigureRenderPart } from './renderer.js'
7
7
  import { DEFAULT_VIEW, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
8
- import { boardFor, changedKeys, componentOf, gizmoResultOf, type Board, type ScaleAnchor } from './stage-model.js'
8
+ import { boardFor, changedKeys, componentOf, gizmoResultOf, needsRefit, type Board, type ScaleAnchor } from './stage-model.js'
9
9
  import { JOINT_FAN } from './joint-fan.js'
10
10
  import { SLOT_MARK } from './slot-layout-host.js'
11
11
  import { PREVIEW, TARGET_FACE } from './command-preview.js'
@@ -48,6 +48,9 @@ interface SceneHandle {
48
48
  root?: SceneLayer
49
49
  }
50
50
 
51
+ /* How long the figure stands still before the camera judges whether to follow it. */
52
+ const REFIT_SETTLE_MS = 400
53
+
51
54
  /* The fan's colour when the figure's palette names no accent. */
52
55
  const FAN_COLOUR = '#ff8a00'
53
56
 
@@ -103,6 +106,9 @@ export class FigureCanvas extends LitElement {
103
106
 
104
107
  private scene?: SceneHandle
105
108
  private board?: Board & { depth: number }
109
+ /** The figure's reach when the view was last fitted to it. */
110
+ private fittedFor?: Board & { depth: number }
111
+ private refitTimer?: ReturnType<typeof setTimeout>
106
112
  private standingFor?: string
107
113
  private watcher?: ResizeObserver
108
114
  /** Set while this canvas writes to components, so the scene's echo of it is not taken for a drag. */
@@ -134,6 +140,7 @@ export class FigureCanvas extends LitElement {
134
140
 
135
141
  disconnectedCallback() {
136
142
  super.disconnectedCallback()
143
+ clearTimeout(this.refitTimer)
137
144
  this.watcher?.disconnect()
138
145
  this.watcher = undefined
139
146
  this.teardown()
@@ -162,6 +169,7 @@ export class FigureCanvas extends LitElement {
162
169
 
163
170
  /** The whole figure in view: the board's fit, then the 3D camera's own. */
164
171
  fit() {
172
+ this.fittedFor = boardFor(this.parts)
165
173
  this.scene?.fit?.('ratio')
166
174
  this.scene?.root?.fitCameraToModel?.()
167
175
  }
@@ -186,6 +194,12 @@ export class FigureCanvas extends LitElement {
186
194
  */
187
195
  if (outgrown) this.grow(needed)
188
196
  this.sync()
197
+ /*
198
+ Grown or shrunk far past the view (a size typed in): the camera follows, once the figure has settled. A value is
199
+ drawn as it is typed -- 「3」, 「30」, 「300」 on the way to 3000 -- so the reach is judged after the last change.
200
+ */
201
+ clearTimeout(this.refitTimer)
202
+ if (this.parts.length) this.refitTimer = setTimeout(() => needsRefit(this.fittedFor, boardFor(this.parts)) && this.fit(), REFIT_SETTLE_MS)
189
203
  return
190
204
  }
191
205
  this.teardown()
@@ -3,6 +3,7 @@ import { css } from 'lit'
3
3
  /** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */
4
4
  export const EditorStyles = css`
5
5
  :host {
6
+ position: relative;
6
7
  display: flex;
7
8
  flex-direction: column;
8
9
  min-height: 0;
@@ -16,6 +17,7 @@ export const EditorStyles = css`
16
17
  fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */
17
18
  ox-canvas-bar button[mode] {
18
19
  height: 28px;
20
+ border: 1px solid transparent;
19
21
  padding: 0 10px;
20
22
  border-radius: 999em;
21
23
  border-color: transparent;
@@ -250,6 +252,63 @@ export const EditorStyles = css`
250
252
  [off] {
251
253
  display: none !important;
252
254
  }
255
+ /* The part's own numbers inside a group the links draw (size, place): one column, the panel's gap. */
256
+ div[group-fields] {
257
+ display: grid;
258
+ gap: 8px;
259
+ }
260
+ /* A folded group (capability, 「더 보기」): its title is the fold's handle, at the title's size. */
261
+ section[more] > details > summary,
262
+ section[capability] > details > summary {
263
+ padding: 0;
264
+ }
265
+ section[more] > details > summary > h3,
266
+ section[capability] > details > summary > h3 {
267
+ margin: 0;
268
+ }
269
+ div[more-body] {
270
+ display: grid;
271
+ gap: 16px;
272
+ margin-top: 12px;
273
+ }
274
+ /*
275
+ The view settings and the release history, opened from the bar over the right of the work: neither is about the
276
+ part chosen, so neither takes a tab beside the part's (chief architect 2026-10-16).
277
+ */
278
+ :host {
279
+ position: relative;
280
+ }
281
+ div[bar-overlay-panel] {
282
+ position: absolute;
283
+ top: var(--figure-bar-height, 44px);
284
+ right: 8px;
285
+ z-index: 5;
286
+ display: flex;
287
+ flex-direction: column;
288
+ width: min(360px, calc(100% - 16px));
289
+ max-height: calc(100% - var(--figure-bar-height, 44px) - 16px);
290
+ background: var(--md-sys-color-surface-container-lowest);
291
+ border: 1px solid var(--md-sys-color-outline-variant);
292
+ border-radius: 8px;
293
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.16);
294
+ }
295
+ div[overlay-head] {
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: space-between;
299
+ padding: 8px 8px 8px 12px;
300
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
301
+ }
302
+ div[overlay-head] h3 {
303
+ margin: 0;
304
+ }
305
+ div[overlay-head] button[quiet] {
306
+ margin-left: 0;
307
+ }
308
+ div[overlay-body] {
309
+ min-height: 0;
310
+ overflow: auto;
311
+ }
253
312
  /* Which part the panel speaks of. */
254
313
  div[subject] {
255
314
  position: sticky;
@@ -277,19 +336,52 @@ export const EditorStyles = css`
277
336
  div[subject-summary] small {
278
337
  color: var(--md-sys-color-on-surface-variant);
279
338
  }
339
+ /*
340
+ No rule here draws every button as a box: the side's buttons take the shared panel rules (SidePanelStyles), one
341
+ height with the fields beside them, and only the bar's publish keeps an outline (user 2026-10-16: 「버튼 박스도
342
+ 과하게 사용되어서」 -- a bare 「button」 rule had put a 32px box round every button the editor drew).
343
+ */
280
344
  button {
281
345
  font: inherit;
282
346
  box-sizing: border-box;
347
+ color: inherit;
348
+ min-width: 0;
349
+ cursor: pointer;
350
+ }
351
+ /* What blocks release, beside 「발행」: one red word button, and its list under the bar. */
352
+ ox-canvas-bar button[release-blocked] {
353
+ display: inline-flex;
354
+ align-items: center;
355
+ gap: 4px;
283
356
  height: 32px;
284
- border: 1px solid var(--md-sys-color-outline-variant);
357
+ padding: 0 10px;
358
+ border: none;
285
359
  border-radius: 6px;
360
+ color: var(--md-sys-color-on-error-container);
361
+ background: var(--md-sys-color-error-container);
362
+ }
363
+ ox-canvas-bar button[release-blocked] md-icon {
364
+ --md-icon-size: 18px;
365
+ }
366
+ div[release-blockers] {
367
+ position: absolute;
368
+ top: 46px;
369
+ right: 8px;
370
+ z-index: 6;
371
+ width: min(340px, calc(100% - 16px));
372
+ padding: 8px 10px 10px;
373
+ box-sizing: border-box;
286
374
  background: var(--md-sys-color-surface);
287
- color: inherit;
288
- padding: 0 8px;
289
- min-width: 0;
375
+ border: 1px solid var(--md-sys-color-outline-variant);
376
+ border-radius: 8px;
377
+ box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);
290
378
  }
291
- button {
292
- cursor: pointer;
379
+ ox-canvas-bar button[publish] {
380
+ height: 32px;
381
+ padding: 0 8px;
382
+ border: 1px solid var(--md-sys-color-outline-variant);
383
+ border-radius: 6px;
384
+ background: var(--md-sys-color-surface);
293
385
  }
294
386
  button:disabled {
295
387
  opacity: 0.45;