@things-factory/figure-ui 10.1.180 → 10.1.183

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 (58) hide show
  1. package/client/modeller/editor-styles.ts +28 -9
  2. package/client/modeller/editor.ts +105 -12
  3. package/client/modeller/fasten-form.ts +207 -0
  4. package/client/modeller/fasten-geometry.ts +5 -1
  5. package/client/modeller/figure-fasten-dialog.ts +442 -0
  6. package/client/modeller/figure-fasten-drawing.ts +32 -11
  7. package/client/modeller/figure-links.ts +56 -300
  8. package/client/modeller/modeller-keys.ts +4 -4
  9. package/client/pages/figure-modeller-page.ts +17 -11
  10. package/dist-client/modeller/editor-styles.js +28 -9
  11. package/dist-client/modeller/editor-styles.js.map +1 -1
  12. package/dist-client/modeller/editor.d.ts +23 -2
  13. package/dist-client/modeller/editor.js +121 -12
  14. package/dist-client/modeller/editor.js.map +1 -1
  15. package/dist-client/modeller/fasten-form.d.ts +69 -0
  16. package/dist-client/modeller/fasten-form.js +131 -0
  17. package/dist-client/modeller/fasten-form.js.map +1 -0
  18. package/dist-client/modeller/fasten-geometry.d.ts +3 -1
  19. package/dist-client/modeller/fasten-geometry.js +6 -1
  20. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  21. package/dist-client/modeller/figure-fasten-dialog.d.ts +64 -0
  22. package/dist-client/modeller/figure-fasten-dialog.js +418 -0
  23. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -0
  24. package/dist-client/modeller/figure-fasten-drawing.d.ts +6 -3
  25. package/dist-client/modeller/figure-fasten-drawing.js +36 -10
  26. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  27. package/dist-client/modeller/figure-links.d.ts +15 -39
  28. package/dist-client/modeller/figure-links.js +58 -301
  29. package/dist-client/modeller/figure-links.js.map +1 -1
  30. package/dist-client/modeller/modeller-keys.js +4 -4
  31. package/dist-client/modeller/modeller-keys.js.map +1 -1
  32. package/dist-client/pages/figure-modeller-page.d.ts +5 -1
  33. package/dist-client/pages/figure-modeller-page.js +14 -12
  34. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  35. package/dist-client/route.d.ts +1 -1
  36. package/dist-client/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +14 -14
  38. package/test/browser/links-page.js +4 -4
  39. package/test/browser/links.check.mjs +184 -46
  40. package/test/browser/modeller-page.js +23 -0
  41. package/test/browser/modeller.check.mjs +19 -20
  42. package/test/browser/namespace-page.js +2 -29
  43. package/test/browser/popup-stand-in.js +48 -0
  44. package/test/browser/product-page.check.mjs +4 -3
  45. package/test/browser/product-page.js +19 -32
  46. package/test/browser/props.check.mjs +28 -7
  47. package/test/browser/screen.check.mjs +28 -23
  48. package/test/browser/section-page.js +13 -41
  49. package/test/browser/section.check.mjs +39 -30
  50. package/test/fasten-form.test.ts +77 -0
  51. package/test/fasten-geometry.test.ts +2 -0
  52. package/test/first-minute.test.ts +7 -4
  53. package/translations/en.json +4 -3
  54. package/translations/es.json +4 -3
  55. package/translations/ja.json +4 -3
  56. package/translations/ko.json +4 -3
  57. package/translations/ms.json +4 -3
  58. package/translations/zh.json +4 -3
@@ -90,11 +90,27 @@ export const EditorStyles = css `
90
90
  color: var(--md-sys-color-primary);
91
91
  font-weight: 600;
92
92
  }
93
- ox-canvas-bar button[save] {
93
+ /*
94
+ The bar's two commitments, one shape (user 2026-10-18: 「발행버튼도 저장버튼도 라운드 버튼으로 … 버튼 안에 적절한 아이콘」):
95
+ round, an icon inside, so neither is taken for the other. Save, done many times a day, is the filled one; release,
96
+ a promise to whoever draws the figure, is outlined.
97
+ */
98
+ ox-canvas-bar button[save],
99
+ ox-canvas-bar button[publish] {
100
+ display: inline-flex;
101
+ align-items: center;
102
+ gap: 6px;
94
103
  height: 32px;
95
- padding: 0 14px;
96
- border: none;
104
+ padding: 0 14px 0 10px;
97
105
  border-radius: 999em;
106
+ font-weight: 600;
107
+ }
108
+ ox-canvas-bar button[save] md-icon,
109
+ ox-canvas-bar button[publish] md-icon {
110
+ --md-icon-size: 18px;
111
+ }
112
+ ox-canvas-bar button[save] {
113
+ border: 1px solid transparent;
98
114
  color: var(--md-sys-color-on-primary);
99
115
  background: var(--md-sys-color-primary);
100
116
  }
@@ -359,9 +375,10 @@ export const EditorStyles = css `
359
375
  align-items: center;
360
376
  gap: 4px;
361
377
  height: 32px;
362
- padding: 0 10px;
378
+ padding: 0 14px 0 10px;
363
379
  border: none;
364
- border-radius: 6px;
380
+ /* Round as save and release beside it: one shape for the bar's end. */
381
+ border-radius: 999em;
365
382
  color: var(--md-sys-color-on-error-container);
366
383
  background: var(--md-sys-color-error-container);
367
384
  }
@@ -382,12 +399,14 @@ export const EditorStyles = css `
382
399
  box-shadow: var(--md-sys-elevation-2, 0 2px 8px #0003);
383
400
  }
384
401
  ox-canvas-bar button[publish] {
385
- height: 32px;
386
- padding: 0 8px;
387
- border: 1px solid var(--md-sys-color-outline-variant);
388
- border-radius: 6px;
402
+ border: 1px solid var(--md-sys-color-outline);
403
+ color: var(--md-sys-color-primary);
389
404
  background: var(--md-sys-color-surface);
390
405
  }
406
+ ox-canvas-bar button[publish][aria-pressed='true'] {
407
+ background: var(--md-sys-color-secondary-container);
408
+ color: var(--md-sys-color-on-secondary-container);
409
+ }
391
410
  button:disabled {
392
411
  opacity: 0.45;
393
412
  cursor: default;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiZ9B,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 ox-canvas-bar button[save] {\n height: 32px;\n padding: 0 14px;\n border: none;\n border-radius: 999em;\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 10px;\n border: none;\n border-radius: 6px;\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 height: 32px;\n padding: 0 8px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n background: var(--md-sys-color-surface);\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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"]}
@@ -9,6 +9,7 @@ import './figure-material-panel.js';
9
9
  import './figure-section-panel.js';
10
10
  import './figure-section-editor.js';
11
11
  import './figure-links.js';
12
+ import './figure-fasten-dialog.js';
12
13
  import './figure-controls.js';
13
14
  import './figure-clips.js';
14
15
  import './figure-data-panel.js';
@@ -63,6 +64,17 @@ export declare class FigureEditor extends LitElement {
63
64
  private tab;
64
65
  /** The view settings or the release history, open over the work from the bar. */
65
66
  private overlay?;
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.
70
+ */
71
+ private fastening;
72
+ private fastenPopup?;
73
+ private fastenWindow?;
74
+ /** Why the window's 「적용」 was refused, said in it. */
75
+ private fastenError;
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. */
77
+ private fastenHover;
66
78
  /** 「더 보기」 open, as this person left it last (chief architect 2026-10-16: kept per person). */
67
79
  private moreOpen;
68
80
  private static storedMoreOpen;
@@ -287,13 +299,22 @@ export declare class FigureEditor extends LitElement {
287
299
  private releaseWords;
288
300
  /** The findings that block release, under the bar beside 「발행」, each with the way out the verdict tab offers for it. */
289
301
  private renderReleaseBlockers;
290
- /** To the release that exists: the history's release section, opened from the bar, not a second one here. */
291
302
  private goRelease;
292
303
  private overhangOf;
293
304
  /** The selected part's motion at both ends of its range, made once per version and part. */
294
305
  private motionEnds;
295
- /** "Fasten it first", from the motion tab: to the properties tab, the fastening form open on this part. */
306
+ /** "Fasten it first", from the motion tab or a row: to the properties tab, the fastening window open on this part. */
296
307
  private goFasten;
308
+ private openFastening;
309
+ 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;
314
+ /** 「적용」: the command, one step; refused, the window stays open and says why. */
315
+ private applyFastening;
316
+ /** A row of the window's list under the pointer or the arrows: the face of that part it would be fastened on, lit. */
317
+ private hoverFastening;
297
318
  /** What the canvas draws of it, made once per preview: a new list every render would stand the board up each time. */
298
319
  private previewShown;
299
320
  /**
@@ -7,7 +7,7 @@ import { openPopup } from '@operato/layout';
7
7
  import { OxPrompt } from '@operato/popup';
8
8
  import { customElement, property, state } from 'lit/decorators.js';
9
9
  import { live } from 'lit/directives/live.js';
10
- import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
10
+ import { figurePartFaceOf, PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figurePartIdFor, figureRecipeIdFor, figureSizeEditChanges, figureScaleAnchorOf, figureSectionMeasuresOf, figurePlaneAlignChanges, figureRowOf, figureFaceOverhangOf, figureRoomOnlyInputsOf, figureRoomRefitOf } from '@hatiolab/figure-model';
11
11
  import './figure-recipe-catalog.js';
12
12
  import { captureFigureThumbnail } from './thumbnail.js';
13
13
  import { drawableFigureParts } from './renderer.js';
@@ -16,10 +16,11 @@ import './figure-parts.js';
16
16
  import './figure-material-panel.js';
17
17
  import './figure-section-panel.js';
18
18
  import './figure-section-editor.js';
19
- import { rowWordsOf } from './command-preview.js';
19
+ import { faceHighlightOf, rowWordsOf } from './command-preview.js';
20
20
  import { CommandPreview, NO_WORDS } from './command-preview-controller.js';
21
21
  import { motionEndsOf } from './motion-ends.js';
22
22
  import './figure-links.js';
23
+ import './figure-fasten-dialog.js';
23
24
  import './figure-controls.js';
24
25
  import './figure-clips.js';
25
26
  import './figure-data-panel.js';
@@ -78,6 +79,15 @@ let FigureEditor = class FigureEditor extends LitElement {
78
79
  this.selected = '';
79
80
  this.error = '';
80
81
  this.tab = 'properties';
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.
85
+ */
86
+ this.fastening = false;
87
+ /** Why the window's 「적용」 was refused, said in it. */
88
+ this.fastenError = '';
89
+ /** 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. */
90
+ this.fastenHover = null;
81
91
  /** 「더 보기」 open, as this person left it last (chief architect 2026-10-16: kept per person). */
82
92
  this.moreOpen = FigureEditor_1.storedMoreOpen();
83
93
  /** What the middle shows: the parts being edited, or the figure placed on a board at eight sizes. */
@@ -136,8 +146,16 @@ let FigureEditor = class FigureEditor extends LitElement {
136
146
  this.lastAnchors = {};
137
147
  /** Data tried in the data tab, given to the preview's figures as their `data`; null when none is (ADR-0051 §8). */
138
148
  this.dataSample = null;
139
- /** To the release that exists: the history's release section, opened from the bar, not a second one here. */
149
+ /*
150
+ The bar's one way to the releases (user 2026-10-18: the history icon and 「발행」 opened the same panel): the panel
151
+ of releases -- what to release now, the releases before it and going back to one -- opened at its release section,
152
+ closed again by a second press.
153
+ */
140
154
  this.goRelease = async () => {
155
+ if (this.overlay === 'history') {
156
+ this.overlay = undefined;
157
+ return;
158
+ }
141
159
  this.overlay = 'history';
142
160
  await this.updateComplete;
143
161
  await this.querySelector('figure-history-panel')?.showRelease?.();
@@ -242,6 +260,9 @@ let FigureEditor = class FigureEditor extends LitElement {
242
260
  this.selected = '';
243
261
  if (_changed.has('selected'))
244
262
  this.gizmoMode = undefined;
263
+ /* 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'))
265
+ this.closeFastening();
245
266
  /*
246
267
  * The section editor is of one part: when the selection moves off it by any way -- another part picked, the scene's
247
268
  * own Esc, the part removed, another figure opened -- it closes and lets go of the preview it held.
@@ -765,10 +786,11 @@ let FigureEditor = class FigureEditor extends LitElement {
765
786
  return html `<figure-links
766
787
  .revision=${shown}
767
788
  .section=${section}
768
- .active=${this.tab === tab}
789
+ .active=${this.tab === tab && !(section === 'shape' && this.fastening)}
769
790
  .partId=${this.selected}
770
791
  ?locked=${unavailable}
771
792
  @command=${(e) => this.linkCommand(e.detail.action)}
793
+ @fasten-open=${() => this.openFastening()}
772
794
  .previewWords=${this.commandPreview.words()}
773
795
  .detachWords=${this.commandPreview.detachWords(shown)}
774
796
  .overhangWords=${section === 'shape' ? this.overhangOf(shown).words : NO_WORDS}
@@ -877,7 +899,7 @@ let FigureEditor = class FigureEditor extends LitElement {
877
899
  <div overlay-body>${body}</div>
878
900
  </div>`;
879
901
  return html `${panel('view', 'figure.label.tab-environment', html `<figure-settings .view=${this.view} @update-view=${(e) => this.setView(e.detail.view)}></figure-settings>`)}
880
- ${panel('history', 'figure.label.history', html `<slot name="history"></slot>`)}`;
902
+ ${panel('history', 'figure.button.release', html `<slot name="history"></slot>`)}`;
881
903
  }
882
904
  /**
883
905
  * The face a size drag keeps for the selected part, figure-model's rule (`figureScaleAnchorOf`): the gizmo shows it
@@ -1429,12 +1451,76 @@ let FigureEditor = class FigureEditor extends LitElement {
1429
1451
  }
1430
1452
  });
1431
1453
  }
1432
- /** "Fasten it first", from the motion tab: to the properties tab, the fastening form open on this part. */
1454
+ /** "Fasten it first", from the motion tab or a row: to the properties tab, the fastening window open on this part. */
1433
1455
  async goFasten() {
1434
1456
  this.tab = 'properties';
1435
1457
  await this.updateComplete;
1436
- const links = [...this.renderRoot.querySelectorAll('figure-links')].find(l => l.section === 'shape');
1437
- await links?.showFastening?.();
1458
+ this.openFastening();
1459
+ }
1460
+ openFastening() {
1461
+ if (!this.selected || this.fastenPopup)
1462
+ return;
1463
+ this.fastenError = '';
1464
+ this.fastenHover = null;
1465
+ 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
+ }
1478
+ closeFastening() {
1479
+ const popup = this.fastenPopup;
1480
+ this.fastenClosed();
1481
+ popup?.close();
1482
+ }
1483
+ fastenClosed() {
1484
+ this.fastenPopup = undefined;
1485
+ this.fastenWindow = undefined;
1486
+ this.fastening = false;
1487
+ this.fastenError = '';
1488
+ this.fastenHover = null;
1489
+ }
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();
1505
+ }
1506
+ /** 「적용」: the command, one step; refused, the window stays open and says why. */
1507
+ applyFastening(action) {
1508
+ if (this.linkCommand(action))
1509
+ this.closeFastening();
1510
+ else
1511
+ this.fastenError = this.error;
1512
+ }
1513
+ /** A row of the window's list under the pointer or the arrows: the face of that part it would be fastened on, lit. */
1514
+ hoverFastening(on) {
1515
+ const shown = this.shown();
1516
+ let lit = null;
1517
+ try {
1518
+ lit = on?.face && shown ? faceHighlightOf(figurePartFaceOf(shown.asset, on.part, on.face, shown.inputs)) : null;
1519
+ }
1520
+ catch {
1521
+ lit = null;
1522
+ }
1523
+ this.fastenHover = lit;
1438
1524
  }
1439
1525
  /** What the canvas draws of it, made once per preview: a new list every render would stand the board up each time. */
1440
1526
  previewShown() {
@@ -1442,7 +1528,9 @@ let FigureEditor = class FigureEditor extends LitElement {
1442
1528
  /* On the motion tab, with nothing being tried: where the selected part's motion takes it, at both ends. */
1443
1529
  if (!p && this.tab === 'motion' && this.middle === 'edit')
1444
1530
  return { ghost: this.motionEnds()?.ghost ?? [], highlight: null };
1445
- return this.commandPreview.drawnOf(p);
1531
+ const drawn = this.commandPreview.drawnOf(p);
1532
+ /* A part in the fastening window's list under the pointer: its face lit in place of the one the command meets. */
1533
+ return this.fastenHover ? { ghost: drawn.ghost, highlight: this.fastenHover } : drawn;
1446
1534
  }
1447
1535
  /**
1448
1536
  * What the author has in view, for the AI dock to hear with a request (ADR-0097 §5.6 item 5): the part selected and
@@ -1485,6 +1573,7 @@ let FigureEditor = class FigureEditor extends LitElement {
1485
1573
  }
1486
1574
  updated(changed) {
1487
1575
  super.updated?.(changed);
1576
+ this.tellFastening();
1488
1577
  /* Nothing blocks release any more: the list under the bar closes, and opens fresh when something does again. */
1489
1578
  if (changed.has('releaseBlockers') && !this.releaseBlockers.length && this.blockersOpen)
1490
1579
  this.blockersOpen = false;
@@ -1697,14 +1786,22 @@ let FigureEditor = class FigureEditor extends LitElement {
1697
1786
  </button>`)}
1698
1787
  <span slot="status" class="status" role="status" ?dirty=${status === 'figure.text.unsaved-changes'}>${i18next.t(status)}</span>
1699
1788
  <span slot="status" release-state>${this.releaseWords(view)}</span>
1700
- ${this.renderOverlayButton('history', 'history', 'figure.label.history', 'commit')}
1701
1789
  ${blockers.length
1702
1790
  ? html `<button slot="commit" release-blocked aria-expanded=${String(this.blockersOpen)} @click=${() => (this.blockersOpen = !this.blockersOpen)}>
1703
1791
  <md-icon>error</md-icon>${i18next.t('figure.button.release-blocked', { count: blockers.length })}
1704
1792
  </button>`
1705
1793
  : nothing}
1706
- <button slot="commit" publish ?disabled=${!view.id} title=${view.id ? '' : i18next.t('figure.text.save-the-figure-before-releasing')} @click=${this.goRelease}>${i18next.t('figure.button.release')}</button>
1707
- <button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>${i18next.t('figure.button.save')}</button>
1794
+ <button
1795
+ slot="commit"
1796
+ publish
1797
+ aria-pressed=${String(this.overlay === 'history')}
1798
+ ?disabled=${!view.id}
1799
+ title=${view.id ? '' : i18next.t('figure.text.save-the-figure-before-releasing')}
1800
+ @click=${this.goRelease}
1801
+ >
1802
+ <md-icon>publish</md-icon>${i18next.t('figure.button.release')}
1803
+ </button>
1804
+ <button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}><md-icon>save</md-icon>${i18next.t('figure.button.save')}</button>
1708
1805
  </ox-canvas-bar>
1709
1806
  ${this.blockersOpen && blockers.length ? this.renderReleaseBlockers(blockers, asset, unavailable) : nothing}
1710
1807
  ${this.catalogOpen
@@ -2098,6 +2195,18 @@ __decorate([
2098
2195
  state(),
2099
2196
  __metadata("design:type", String)
2100
2197
  ], FigureEditor.prototype, "overlay", void 0);
2198
+ __decorate([
2199
+ state(),
2200
+ __metadata("design:type", Object)
2201
+ ], FigureEditor.prototype, "fastening", void 0);
2202
+ __decorate([
2203
+ state(),
2204
+ __metadata("design:type", Object)
2205
+ ], FigureEditor.prototype, "fastenError", void 0);
2206
+ __decorate([
2207
+ state(),
2208
+ __metadata("design:type", Object)
2209
+ ], FigureEditor.prototype, "fastenHover", void 0);
2101
2210
  __decorate([
2102
2211
  state(),
2103
2212
  __metadata("design:type", Object)