@things-factory/figure-ui 10.1.152 → 10.1.153

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 (56) hide show
  1. package/client/modeller/ai-proposal.ts +14 -13
  2. package/client/modeller/command-preview-controller.ts +210 -0
  3. package/client/modeller/editor-styles.ts +307 -0
  4. package/client/modeller/editor.ts +58 -619
  5. package/client/modeller/figure-first-save.ts +9 -1
  6. package/client/modeller/modeller-keys.ts +60 -0
  7. package/client/modeller/pane-fold.ts +61 -0
  8. package/client/modeller/panel-memory.ts +53 -0
  9. package/client/modeller/refusal-teller.ts +93 -0
  10. package/client/modeller/refusals.ts +18 -1
  11. package/dist-client/modeller/ai-proposal.d.ts +6 -5
  12. package/dist-client/modeller/ai-proposal.js +9 -9
  13. package/dist-client/modeller/ai-proposal.js.map +1 -1
  14. package/dist-client/modeller/clips.d.ts +1 -1
  15. package/dist-client/modeller/command-preview-controller.d.ts +75 -0
  16. package/dist-client/modeller/command-preview-controller.js +178 -0
  17. package/dist-client/modeller/command-preview-controller.js.map +1 -0
  18. package/dist-client/modeller/editor-styles.d.ts +2 -0
  19. package/dist-client/modeller/editor-styles.js +307 -0
  20. package/dist-client/modeller/editor-styles.js.map +1 -0
  21. package/dist-client/modeller/editor.d.ts +5 -50
  22. package/dist-client/modeller/editor.js +69 -641
  23. package/dist-client/modeller/editor.js.map +1 -1
  24. package/dist-client/modeller/figure-first-save.d.ts +1 -0
  25. package/dist-client/modeller/figure-first-save.js +8 -1
  26. package/dist-client/modeller/figure-first-save.js.map +1 -1
  27. package/dist-client/modeller/modeller-keys.d.ts +22 -0
  28. package/dist-client/modeller/modeller-keys.js +43 -0
  29. package/dist-client/modeller/modeller-keys.js.map +1 -0
  30. package/dist-client/modeller/pane-fold.d.ts +15 -0
  31. package/dist-client/modeller/pane-fold.js +60 -0
  32. package/dist-client/modeller/pane-fold.js.map +1 -0
  33. package/dist-client/modeller/panel-memory.d.ts +15 -0
  34. package/dist-client/modeller/panel-memory.js +54 -0
  35. package/dist-client/modeller/panel-memory.js.map +1 -0
  36. package/dist-client/modeller/refusal-teller.d.ts +36 -0
  37. package/dist-client/modeller/refusal-teller.js +81 -0
  38. package/dist-client/modeller/refusal-teller.js.map +1 -0
  39. package/dist-client/modeller/refusals.d.ts +10 -0
  40. package/dist-client/modeller/refusals.js +9 -0
  41. package/dist-client/modeller/refusals.js.map +1 -1
  42. package/dist-client/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +4 -4
  44. package/test/browser/fix-alignment.check.mjs +13 -2
  45. package/test/browser/links.check.mjs +36 -9
  46. package/test/browser/modeller.check.mjs +25 -0
  47. package/test/browser/parts-page.js +9 -1
  48. package/test/browser/parts.check.mjs +22 -0
  49. package/test/browser/places-mesh.check.mjs +25 -9
  50. package/test/browser/product-page.check.mjs +42 -2
  51. package/test/browser/screen.check.mjs +33 -0
  52. package/test/clip-ai-proposal.test.ts +4 -1
  53. package/test/clip-consent.test.ts +9 -2
  54. package/test/layout.test.ts +3 -1
  55. package/test/refusals.test.ts +18 -10
  56. package/test/theme.test.ts +5 -15
@@ -17,6 +17,7 @@ export declare class FigureFirstSave extends LitElement {
17
17
  /** The identifier offered, while the author has not changed it. */
18
18
  private suggested;
19
19
  willUpdate(changed: Map<string, unknown>): void;
20
+ firstUpdated(): void;
20
21
  render(): import("lit-html").TemplateResult<1>;
21
22
  private onKey;
22
23
  private confirm;
@@ -73,6 +73,14 @@ let FigureFirstSave = class FigureFirstSave extends LitElement {
73
73
  if (offer.suggested !== undefined)
74
74
  this.suggested = offer.suggested;
75
75
  }
76
+ /*
77
+ The name is focused here, each time the question opens. The browser honours `autofocus` once per document: the
78
+ question asked again opened with nothing focused, and any autofocus taken earlier on the page left even the first
79
+ opening unfocused (2026-10-11, product-page check).
80
+ */
81
+ firstUpdated() {
82
+ this.renderRoot.querySelector('#name')?.focus();
83
+ }
76
84
  render() {
77
85
  return html `
78
86
  <div class="what">${i18next.t('figure.text.save-new-figure-desc')}</div>
@@ -80,7 +88,6 @@ let FigureFirstSave = class FigureFirstSave extends LitElement {
80
88
  <label for="name">${i18next.t('figure.label.name')}</label>
81
89
  <input
82
90
  id="name"
83
- autofocus
84
91
  .value=${this.name}
85
92
  placeholder=${i18next.t('figure.text.figure-name-example')}
86
93
  @input=${(e) => (this.name = e.target.value)}
@@ -1 +1 @@
1
- {"version":3,"file":"figure-first-save.js","sourceRoot":"","sources":["../../client/modeller/figure-first-save.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { closePopup } from '@operato/popup'\nimport '@operato/popup/ox-popup-actions.js'\nimport { CommonPopupStyles, FormFieldStyles } from '@operato/styles'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/text-button.js'\n\nimport { firstSaveRefusalOf, offeredTypeOf, releasableType, typeOf, type FirstSave } from './first-save.js'\n\n/**\n * The first save of a new figure: its type and its name, asked once, in the suite's popup look.\n * Closes with the answer, or with nothing when cancelled -- then nothing is saved.\n */\n@customElement('figure-first-save')\nexport class FigureFirstSave extends LitElement {\n static styles = [\n FormFieldStyles,\n CommonPopupStyles,\n css`\n /* The popup's height is a share of the window (small is 40%): on a low window the buttons must still be reached. */\n :host {\n overflow-y: auto;\n }\n div[field] {\n display: grid;\n gap: 4px;\n }\n div[field] label {\n font-size: var(--popup-font-size-small);\n color: var(--md-sys-color-on-surface-variant);\n }\n div[field] input {\n box-sizing: border-box;\n width: 100%;\n }\n input[type-field] {\n font-family: var(--mono-font, monospace);\n }\n `\n ]\n\n /** The type the figure has so far -- empty for a temporary one, which is not offered. */\n @property() type = ''\n @property() name = ''\n /** Type names already used in this domain. */\n @property({ attribute: false }) taken: string[] = []\n @state() private refusal: { key: string; type?: string } | null = null\n /** The identifier offered, while the author has not changed it. */\n @state() private suggested = ''\n\n /*\n Asked for a name first, with a valid identifier already there to keep or change (Q-D3). The identifier is not the\n name: renaming does not change it, and a retried first save opens on the one answered before (the page keeps it).\n Only an offered one found taken -- told by the server, or in the list -- is offered again; one the author typed is\n kept as typed, and its refusal said.\n */\n willUpdate(changed: Map<string, unknown>) {\n if (!(changed.has('type') || changed.has('taken'))) return\n const offer = offeredTypeOf(this.type, this.name, this.taken)\n this.type = offer.type\n if (offer.suggested !== undefined) this.suggested = offer.suggested\n }\n\n render() {\n return html`\n <div class=\"what\">${i18next.t('figure.text.save-new-figure-desc')}</div>\n <div field>\n <label for=\"name\">${i18next.t('figure.label.name')}</label>\n <input\n id=\"name\"\n autofocus\n .value=${this.name}\n placeholder=${i18next.t('figure.text.figure-name-example')}\n @input=${(e: Event) => (this.name = (e.target as HTMLInputElement).value)}\n @keydown=${this.onKey}\n />\n </div>\n <div field>\n <label for=\"type\">${i18next.t('figure.label.figure-identifier')}</label>\n <input\n id=\"type\"\n type-field\n .value=${this.type}\n placeholder=${i18next.t('figure.text.figure-type-example')}\n @input=${(e: Event) => {\n this.type = typeOf((e.target as HTMLInputElement).value)\n ;(e.target as HTMLInputElement).value = this.type\n this.refusal = null\n }}\n @keydown=${this.onKey}\n />\n ${this.type && !releasableType(this.type)\n ? html`<div class=\"note\">${i18next.t('figure.text.type-name-will-not-release')}</div>`\n : this.suggested && this.type === this.suggested\n ? html`<div class=\"note\">${i18next.t('figure.text.identifier-suggested')}</div>`\n : nothing}\n </div>\n ${this.refusal ? html`<div class=\"failure\" role=\"alert\">${i18next.t(this.refusal.key, { type: this.refusal.type })}</div>` : nothing}\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => closePopup(this)}>${i18next.t('figure.button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${!this.type} @click=${this.confirm}>${i18next.t('figure.button.save')}</md-filled-button>\n </ox-popup-actions>\n `\n }\n\n private onKey = (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.isComposing) this.confirm()\n }\n\n private confirm = () => {\n this.refusal = firstSaveRefusalOf(this.type, this.taken)\n if (this.refusal) return\n closePopup(this, { type: this.type, name: this.name.trim() || this.type } satisfies FirstSave)\n }\n}\n"]}
1
+ {"version":3,"file":"figure-first-save.js","sourceRoot":"","sources":["../../client/modeller/figure-first-save.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { i18next } from '@operato/i18n'\nimport { closePopup } from '@operato/popup'\nimport '@operato/popup/ox-popup-actions.js'\nimport { CommonPopupStyles, FormFieldStyles } from '@operato/styles'\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/text-button.js'\n\nimport { firstSaveRefusalOf, offeredTypeOf, releasableType, typeOf, type FirstSave } from './first-save.js'\n\n/**\n * The first save of a new figure: its type and its name, asked once, in the suite's popup look.\n * Closes with the answer, or with nothing when cancelled -- then nothing is saved.\n */\n@customElement('figure-first-save')\nexport class FigureFirstSave extends LitElement {\n static styles = [\n FormFieldStyles,\n CommonPopupStyles,\n css`\n /* The popup's height is a share of the window (small is 40%): on a low window the buttons must still be reached. */\n :host {\n overflow-y: auto;\n }\n div[field] {\n display: grid;\n gap: 4px;\n }\n div[field] label {\n font-size: var(--popup-font-size-small);\n color: var(--md-sys-color-on-surface-variant);\n }\n div[field] input {\n box-sizing: border-box;\n width: 100%;\n }\n input[type-field] {\n font-family: var(--mono-font, monospace);\n }\n `\n ]\n\n /** The type the figure has so far -- empty for a temporary one, which is not offered. */\n @property() type = ''\n @property() name = ''\n /** Type names already used in this domain. */\n @property({ attribute: false }) taken: string[] = []\n @state() private refusal: { key: string; type?: string } | null = null\n /** The identifier offered, while the author has not changed it. */\n @state() private suggested = ''\n\n /*\n Asked for a name first, with a valid identifier already there to keep or change (Q-D3). The identifier is not the\n name: renaming does not change it, and a retried first save opens on the one answered before (the page keeps it).\n Only an offered one found taken -- told by the server, or in the list -- is offered again; one the author typed is\n kept as typed, and its refusal said.\n */\n willUpdate(changed: Map<string, unknown>) {\n if (!(changed.has('type') || changed.has('taken'))) return\n const offer = offeredTypeOf(this.type, this.name, this.taken)\n this.type = offer.type\n if (offer.suggested !== undefined) this.suggested = offer.suggested\n }\n\n /*\n The name is focused here, each time the question opens. The browser honours `autofocus` once per document: the\n question asked again opened with nothing focused, and any autofocus taken earlier on the page left even the first\n opening unfocused (2026-10-11, product-page check).\n */\n firstUpdated() {\n this.renderRoot.querySelector<HTMLInputElement>('#name')?.focus()\n }\n\n render() {\n return html`\n <div class=\"what\">${i18next.t('figure.text.save-new-figure-desc')}</div>\n <div field>\n <label for=\"name\">${i18next.t('figure.label.name')}</label>\n <input\n id=\"name\"\n .value=${this.name}\n placeholder=${i18next.t('figure.text.figure-name-example')}\n @input=${(e: Event) => (this.name = (e.target as HTMLInputElement).value)}\n @keydown=${this.onKey}\n />\n </div>\n <div field>\n <label for=\"type\">${i18next.t('figure.label.figure-identifier')}</label>\n <input\n id=\"type\"\n type-field\n .value=${this.type}\n placeholder=${i18next.t('figure.text.figure-type-example')}\n @input=${(e: Event) => {\n this.type = typeOf((e.target as HTMLInputElement).value)\n ;(e.target as HTMLInputElement).value = this.type\n this.refusal = null\n }}\n @keydown=${this.onKey}\n />\n ${this.type && !releasableType(this.type)\n ? html`<div class=\"note\">${i18next.t('figure.text.type-name-will-not-release')}</div>`\n : this.suggested && this.type === this.suggested\n ? html`<div class=\"note\">${i18next.t('figure.text.identifier-suggested')}</div>`\n : nothing}\n </div>\n ${this.refusal ? html`<div class=\"failure\" role=\"alert\">${i18next.t(this.refusal.key, { type: this.refusal.type })}</div>` : nothing}\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => closePopup(this)}>${i18next.t('figure.button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${!this.type} @click=${this.confirm}>${i18next.t('figure.button.save')}</md-filled-button>\n </ox-popup-actions>\n `\n }\n\n private onKey = (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.isComposing) this.confirm()\n }\n\n private confirm = () => {\n this.refusal = firstSaveRefusalOf(this.type, this.taken)\n if (this.refusal) return\n closePopup(this, { type: this.type, name: this.name.trim() || this.type } satisfies FirstSave)\n }\n}\n"]}
@@ -0,0 +1,22 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ import { type KeyAction } from './keys.js';
3
+ /** What the keys ask of the editor. */
4
+ export interface ModellerKeysHost {
5
+ /** Whether the modeller takes keys now: a figure open, not loading or saving, and no section editor open. */
6
+ taking(): boolean;
7
+ /** The scene, whose keys the modeller's are too. */
8
+ scene(): Element | null;
9
+ selected(): string;
10
+ act(action: KeyAction): void;
11
+ }
12
+ /**
13
+ * The modeller's keys (`keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. Heard on
14
+ * the window, first (capture), while the editor is on the page.
15
+ */
16
+ export declare class ModellerKeys implements ReactiveController {
17
+ private readonly of;
18
+ constructor(host: ReactiveControllerHost, of: ModellerKeysHost);
19
+ hostConnected(): void;
20
+ hostDisconnected(): void;
21
+ private onKey;
22
+ }
@@ -0,0 +1,43 @@
1
+ import { keyActionOf } from './keys.js';
2
+ /**
3
+ * The modeller's keys (`keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. Heard on
4
+ * the window, first (capture), while the editor is on the page.
5
+ */
6
+ export class ModellerKeys {
7
+ constructor(host, of) {
8
+ this.of = of;
9
+ this.onKey = (e) => {
10
+ if (!this.of.taking())
11
+ return;
12
+ const canvas = this.of.scene();
13
+ const path = e.composedPath();
14
+ const action = keyActionOf({
15
+ key: e.key,
16
+ ctrlKey: e.ctrlKey,
17
+ metaKey: e.metaKey,
18
+ shiftKey: e.shiftKey,
19
+ altKey: e.altKey,
20
+ target: path[0],
21
+ inScene: !!canvas && path.includes(canvas)
22
+ }, this.of.selected());
23
+ if (!action)
24
+ return;
25
+ e.preventDefault();
26
+ /*
27
+ The session's history is the only one. With the canvas focused the scene took Ctrl+Z as well, undoing on its own
28
+ board the last value the canvas had written -- which the canvas then told as a drag: an undo of a drag on a
29
+ part placed by a rule wrote the drag again (2026-09-25). Heard first (capture) and kept from the scene.
30
+ */
31
+ e.stopPropagation();
32
+ this.of.act(action);
33
+ };
34
+ host.addController(this);
35
+ }
36
+ hostConnected() {
37
+ window.addEventListener('keydown', this.onKey, true);
38
+ }
39
+ hostDisconnected() {
40
+ window.removeEventListener('keydown', this.onKey, true);
41
+ }
42
+ }
43
+ //# sourceMappingURL=modeller-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modeller-keys.js","sourceRoot":"","sources":["../../client/modeller/modeller-keys.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAkB,MAAM,WAAW,CAAA;AAYvD;;;GAGG;AACH,MAAM,OAAO,YAAY;IACvB,YACE,IAA4B,EACX,EAAoB;QAApB,OAAE,GAAF,EAAE,CAAkB;QAa/B,UAAK,GAAG,CAAC,CAAgB,EAAE,EAAE;YACnC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE;gBAAE,OAAM;YAC7B,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAA;YAC9B,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,CAAA;YAC7B,MAAM,MAAM,GAAG,WAAW,CACxB;gBACE,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,MAAM,EAAE,IAAI,CAAC,CAAC,CAAgB;gBAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;aAC3C,EACD,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CACnB,CAAA;YACD,IAAI,CAAC,MAAM;gBAAE,OAAM;YACnB,CAAC,CAAC,cAAc,EAAE,CAAA;YAClB;;;;cAIE;YACF,CAAC,CAAC,eAAe,EAAE,CAAA;YACnB,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACrB,CAAC,CAAA;QApCC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAED,aAAa;QACX,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACtD,CAAC;IAED,gBAAgB;QACd,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IACzD,CAAC;CA4BF","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { keyActionOf, type KeyAction } from './keys.js'\n\n/** What the keys ask of the editor. */\nexport interface ModellerKeysHost {\n /** Whether the modeller takes keys now: a figure open, not loading or saving, and no section editor open. */\n taking(): boolean\n /** The scene, whose keys the modeller's are too. */\n scene(): Element | null\n selected(): string\n act(action: KeyAction): void\n}\n\n/**\n * The modeller's keys (`keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. Heard on\n * the window, first (capture), while the editor is on the page.\n */\nexport class ModellerKeys implements ReactiveController {\n constructor(\n host: ReactiveControllerHost,\n private readonly of: ModellerKeysHost\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n window.addEventListener('keydown', this.onKey, true)\n }\n\n hostDisconnected() {\n window.removeEventListener('keydown', this.onKey, true)\n }\n\n private onKey = (e: KeyboardEvent) => {\n if (!this.of.taking()) return\n const canvas = this.of.scene()\n const path = e.composedPath()\n const action = keyActionOf(\n {\n key: e.key,\n ctrlKey: e.ctrlKey,\n metaKey: e.metaKey,\n shiftKey: e.shiftKey,\n altKey: e.altKey,\n target: path[0] as HTMLElement,\n inScene: !!canvas && path.includes(canvas)\n },\n this.of.selected()\n )\n if (!action) return\n e.preventDefault()\n /*\n The session's history is the only one. With the canvas focused the scene took Ctrl+Z as well, undoing on its own\n board the last value the canvas had written -- which the canvas then told as a drag: an undo of a drag on a\n part placed by a rule wrote the drag again (2026-09-25). Heard first (capture) and kept from the scene.\n */\n e.stopPropagation()\n this.of.act(action)\n }\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { ReactiveController, ReactiveControllerHost } from 'lit';
2
+ import type { OxThreePane } from '@operato/layout';
3
+ export declare class PaneFold implements ReactiveController {
4
+ private readonly host;
5
+ private readonly panes;
6
+ static readonly FOLD_START_UNDER = 1100;
7
+ private foldedForWidth;
8
+ private keptOpenByAuthor;
9
+ private widthWatch;
10
+ /** `panes` is the frame the bar folds, once it is drawn. */
11
+ constructor(host: ReactiveControllerHost & HTMLElement, panes: () => OxThreePane | undefined);
12
+ hostConnected(): void;
13
+ hostDisconnected(): void;
14
+ fitTo(width: number): void;
15
+ }
@@ -0,0 +1,60 @@
1
+ import { i18next } from '@operato/i18n';
2
+ import { notify } from '@operato/layout';
3
+ /** Whether the narrow fold was said in this browser before; true from now on. Storage refused: said, as before. */
4
+ function foldSaid() {
5
+ try {
6
+ if (localStorage.getItem('figure-fold-said'))
7
+ return true;
8
+ localStorage.setItem('figure-fold-said', '1');
9
+ }
10
+ catch {
11
+ /* No storage here: said every time, as it was. */
12
+ }
13
+ return false;
14
+ }
15
+ /*
16
+ Narrow, the parts list gives way to the canvas rather than all three shrinking (designer 2026-09-27, U11): under
17
+ FOLD_START_UNDER px the canvas would be left under about 560 px beside both sides. The fold is said once, and the
18
+ bar's own toggle opens it again; opened by the author, it is not folded again until the screen has been wide.
19
+ Said once in this browser, not on every opening (2026-10-02): the list folds as before, the notice does not come back.
20
+ */
21
+ export class PaneFold {
22
+ static { this.FOLD_START_UNDER = 1100; }
23
+ /** `panes` is the frame the bar folds, once it is drawn. */
24
+ constructor(host, panes) {
25
+ this.host = host;
26
+ this.panes = panes;
27
+ this.foldedForWidth = false;
28
+ this.keptOpenByAuthor = false;
29
+ this.widthWatch = new ResizeObserver(([entry]) => this.fitTo(entry.contentRect.width));
30
+ host.addController(this);
31
+ }
32
+ hostConnected() {
33
+ this.widthWatch.observe(this.host);
34
+ }
35
+ hostDisconnected() {
36
+ this.widthWatch.disconnect();
37
+ }
38
+ fitTo(width) {
39
+ const panes = this.panes();
40
+ if (!panes || !width)
41
+ return;
42
+ if (this.foldedForWidth && !panes.foldStart)
43
+ this.keptOpenByAuthor = true;
44
+ if (width < PaneFold.FOLD_START_UNDER) {
45
+ if (!panes.foldStart && !this.keptOpenByAuthor) {
46
+ panes.foldStart = true;
47
+ this.foldedForWidth = true;
48
+ if (!foldSaid())
49
+ notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') });
50
+ }
51
+ }
52
+ else {
53
+ if (this.foldedForWidth && panes.foldStart)
54
+ panes.foldStart = false;
55
+ this.foldedForWidth = false;
56
+ this.keptOpenByAuthor = false;
57
+ }
58
+ }
59
+ }
60
+ //# sourceMappingURL=pane-fold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pane-fold.js","sourceRoot":"","sources":["../../client/modeller/pane-fold.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAExC,mHAAmH;AACnH,SAAS,QAAQ;IACf,IAAI,CAAC;QACH,IAAI,YAAY,CAAC,OAAO,CAAC,kBAAkB,CAAC;YAAE,OAAO,IAAI,CAAA;QACzD,YAAY,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;EAKE;AACF,MAAM,OAAO,QAAQ;aACH,qBAAgB,GAAG,IAAI,AAAP,CAAO;IAKvC,4DAA4D;IAC5D,YACmB,IAA0C,EAC1C,KAAoC;QADpC,SAAI,GAAJ,IAAI,CAAsC;QAC1C,UAAK,GAAL,KAAK,CAA+B;QAP/C,mBAAc,GAAG,KAAK,CAAA;QACtB,qBAAgB,GAAG,KAAK,CAAA;QACxB,eAAU,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;QAOxF,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAED,aAAa;QACX,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpC,CAAC;IAED,gBAAgB;QACd,IAAI,CAAC,UAAU,CAAC,UAAU,EAAE,CAAA;IAC9B,CAAC;IAED,KAAK,CAAC,KAAa;QACjB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QAC1B,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK;YAAE,OAAM;QAC5B,IAAI,IAAI,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,SAAS;YAAE,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAA;QACzE,IAAI,KAAK,GAAG,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YACtC,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAC/C,KAAK,CAAC,SAAS,GAAG,IAAI,CAAA;gBACtB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAA;gBAC1B,IAAI,CAAC,QAAQ,EAAE;oBAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,uCAAuC,CAAC,EAAE,CAAC,CAAA;YACzG,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,IAAI,CAAC,cAAc,IAAI,KAAK,CAAC,SAAS;gBAAE,KAAK,CAAC,SAAS,GAAG,KAAK,CAAA;YACnE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAA;YAC3B,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAA;QAC/B,CAAC;IACH,CAAC","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport type { OxThreePane } from '@operato/layout'\nimport { notify } from '@operato/layout'\n\n/** Whether the narrow fold was said in this browser before; true from now on. Storage refused: said, as before. */\nfunction foldSaid(): boolean {\n try {\n if (localStorage.getItem('figure-fold-said')) return true\n localStorage.setItem('figure-fold-said', '1')\n } catch {\n /* No storage here: said every time, as it was. */\n }\n return false\n}\n\n/*\n Narrow, the parts list gives way to the canvas rather than all three shrinking (designer 2026-09-27, U11): under\n FOLD_START_UNDER px the canvas would be left under about 560 px beside both sides. The fold is said once, and the\n bar's own toggle opens it again; opened by the author, it is not folded again until the screen has been wide.\n Said once in this browser, not on every opening (2026-10-02): the list folds as before, the notice does not come back.\n*/\nexport class PaneFold implements ReactiveController {\n static readonly FOLD_START_UNDER = 1100\n private foldedForWidth = false\n private keptOpenByAuthor = false\n private widthWatch = new ResizeObserver(([entry]) => this.fitTo(entry!.contentRect.width))\n\n /** `panes` is the frame the bar folds, once it is drawn. */\n constructor(\n private readonly host: ReactiveControllerHost & HTMLElement,\n private readonly panes: () => OxThreePane | undefined\n ) {\n host.addController(this)\n }\n\n hostConnected() {\n this.widthWatch.observe(this.host)\n }\n\n hostDisconnected() {\n this.widthWatch.disconnect()\n }\n\n fitTo(width: number) {\n const panes = this.panes()\n if (!panes || !width) return\n if (this.foldedForWidth && !panes.foldStart) this.keptOpenByAuthor = true\n if (width < PaneFold.FOLD_START_UNDER) {\n if (!panes.foldStart && !this.keptOpenByAuthor) {\n panes.foldStart = true\n this.foldedForWidth = true\n if (!foldSaid()) notify({ level: 'info', message: i18next.t('figure.text.parts-panel-folded-narrow') })\n }\n } else {\n if (this.foldedForWidth && panes.foldStart) panes.foldStart = false\n this.foldedForWidth = false\n this.keptOpenByAuthor = false\n }\n }\n}\n"]}
@@ -0,0 +1,15 @@
1
+ export declare class PanelMemory {
2
+ private readonly root;
3
+ private kept;
4
+ private restoreFor?;
5
+ /** `root` is where the properties panel is drawn: the editor's render root. */
6
+ constructor(root: () => ParentNode);
7
+ private panel;
8
+ private foldsOf;
9
+ /** Another model: nothing kept. */
10
+ clear(): void;
11
+ /** The selection moves from `was` to `now` ('' is the model): the panel as `was` left it is kept, `now`'s put back after the update. */
12
+ leave(was: string, now: string): void;
13
+ /** After the update: the panel of the part chosen as it was left, while that part is still the one chosen. */
14
+ restore(selected: () => string): void;
15
+ }
@@ -0,0 +1,54 @@
1
+ /*
2
+ Per model and per part, how far its properties panel was scrolled and which of its folds were open, so going back
3
+ to one finds it as it was left (designer 2026-09-27, B.1). The path stays held at the top, so a scroll kept cannot
4
+ hide which part the panel speaks of. Another model starts clean.
5
+ */
6
+ export class PanelMemory {
7
+ /** `root` is where the properties panel is drawn: the editor's render root. */
8
+ constructor(root) {
9
+ this.root = root;
10
+ this.kept = new Map();
11
+ }
12
+ panel() {
13
+ return this.root().querySelector('div[panel]');
14
+ }
15
+ foldsOf(panel) {
16
+ const folds = [...panel.querySelectorAll('details')];
17
+ for (const inner of panel.querySelectorAll('figure-links'))
18
+ folds.push(...(inner.shadowRoot?.querySelectorAll('details') ?? []));
19
+ return folds;
20
+ }
21
+ /** Another model: nothing kept. */
22
+ clear() {
23
+ this.kept.clear();
24
+ }
25
+ /** The selection moves from `was` to `now` ('' is the model): the panel as `was` left it is kept, `now`'s put back after the update. */
26
+ leave(was, now) {
27
+ const panel = this.panel();
28
+ if (panel)
29
+ this.kept.set(was || '@model', { top: panel.scrollTop, open: this.foldsOf(panel).map(d => d.open) });
30
+ this.restoreFor = now || '@model';
31
+ }
32
+ /** After the update: the panel of the part chosen as it was left, while that part is still the one chosen. */
33
+ restore(selected) {
34
+ if (this.restoreFor === undefined)
35
+ return;
36
+ const key = this.restoreFor;
37
+ this.restoreFor = undefined;
38
+ /*
39
+ Put back as soon as the links have drawn, before any click can come: the updates are microtasks. Put back a
40
+ frame later, it closed a fold opened by a click in that frame (2026-10-11, the row on a bench under load).
41
+ */
42
+ const panel = this.panel();
43
+ if (panel)
44
+ void Promise.all([...panel.querySelectorAll('figure-links')].map(el => el.updateComplete)).then(() => {
45
+ if ((selected() || '@model') !== key)
46
+ return;
47
+ const kept = this.kept.get(key);
48
+ if (kept)
49
+ this.foldsOf(panel).forEach((fold, i) => i < kept.open.length && (fold.open = kept.open[i]));
50
+ panel.scrollTop = kept?.top ?? 0;
51
+ });
52
+ }
53
+ }
54
+ //# sourceMappingURL=panel-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-memory.js","sourceRoot":"","sources":["../../client/modeller/panel-memory.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AACF,MAAM,OAAO,WAAW;IAItB,+EAA+E;IAC/E,YAA6B,IAAsB;QAAtB,SAAI,GAAJ,IAAI,CAAkB;QAJ3C,SAAI,GAAG,IAAI,GAAG,EAA4C,CAAA;IAIZ,CAAC;IAE/C,KAAK;QACX,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,aAAa,CAAc,YAAY,CAAC,CAAA;IAC7D,CAAC;IAEO,OAAO,CAAC,KAAkB;QAChC,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAA;QACpD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,gBAAgB,CAAC,cAAc,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,gBAAgB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;QAChI,OAAO,KAAK,CAAA;IACd,CAAC;IAED,mCAAmC;IACnC,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAA;IACnB,CAAC;IAED,wIAAwI;IACxI,KAAK,CAAC,GAAW,EAAE,GAAW;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QAC1B,IAAI,KAAK;YAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,QAAQ,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC/G,IAAI,CAAC,UAAU,GAAG,GAAG,IAAI,QAAQ,CAAA;IACnC,CAAC;IAED,8GAA8G;IAC9G,OAAO,CAAC,QAAsB;QAC5B,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;YAAE,OAAM;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAA;QAC3B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAA;QAC3B;;;UAGE;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAA;QAC1B,IAAI,KAAK;YACP,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,gBAAgB,CAAsD,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;gBACxJ,IAAI,CAAC,QAAQ,EAAE,IAAI,QAAQ,CAAC,KAAK,GAAG;oBAAE,OAAM;gBAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;gBAC/B,IAAI,IAAI;oBAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC,CAAA;gBACvG,KAAK,CAAC,SAAS,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAA;YAClC,CAAC,CAAC,CAAA;IACN,CAAC;CACF","sourcesContent":["/*\n Per model and per part, how far its properties panel was scrolled and which of its folds were open, so going back\n to one finds it as it was left (designer 2026-09-27, B.1). The path stays held at the top, so a scroll kept cannot\n hide which part the panel speaks of. Another model starts clean.\n*/\nexport class PanelMemory {\n private kept = new Map<string, { top: number; open: boolean[] }>()\n private restoreFor?: string\n\n /** `root` is where the properties panel is drawn: the editor's render root. */\n constructor(private readonly root: () => ParentNode) {}\n\n private panel() {\n return this.root().querySelector<HTMLElement>('div[panel]')\n }\n\n private foldsOf(panel: HTMLElement): HTMLDetailsElement[] {\n const folds = [...panel.querySelectorAll('details')]\n for (const inner of panel.querySelectorAll('figure-links')) folds.push(...(inner.shadowRoot?.querySelectorAll('details') ?? []))\n return folds\n }\n\n /** Another model: nothing kept. */\n clear() {\n this.kept.clear()\n }\n\n /** The selection moves from `was` to `now` ('' is the model): the panel as `was` left it is kept, `now`'s put back after the update. */\n leave(was: string, now: string) {\n const panel = this.panel()\n if (panel) this.kept.set(was || '@model', { top: panel.scrollTop, open: this.foldsOf(panel).map(d => d.open) })\n this.restoreFor = now || '@model'\n }\n\n /** After the update: the panel of the part chosen as it was left, while that part is still the one chosen. */\n restore(selected: () => string) {\n if (this.restoreFor === undefined) return\n const key = this.restoreFor\n this.restoreFor = undefined\n /*\n Put back as soon as the links have drawn, before any click can come: the updates are microtasks. Put back a\n frame later, it closed a fold opened by a click in that frame (2026-10-11, the row on a bench under load).\n */\n const panel = this.panel()\n if (panel)\n void Promise.all([...panel.querySelectorAll<HTMLElement & { updateComplete?: Promise<unknown> }>('figure-links')].map(el => el.updateComplete)).then(() => {\n if ((selected() || '@model') !== key) return\n const kept = this.kept.get(key)\n if (kept) this.foldsOf(panel).forEach((fold, i) => i < kept.open.length && (fold.open = kept.open[i]!))\n panel.scrollTop = kept?.top ?? 0\n })\n }\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import type { PropertyValues } from 'lit';
2
+ import type { FigureAuthoringAction } from '@hatiolab/figure-model';
3
+ import type { FigureProductSession } from './product-session.js';
4
+ /** The same link offered again with the size range narrowed, after a link the declared range could not hold. */
5
+ export type Retry = {
6
+ label: string;
7
+ action?: FigureAuthoringAction;
8
+ run?: () => void;
9
+ };
10
+ /** What the teller reads of the editor, and what it asks of it. */
11
+ export interface RefusalHost {
12
+ session(): FigureProductSession | undefined;
13
+ error(): string;
14
+ notice(): string;
15
+ retry(): Retry | undefined;
16
+ /** Said as the editor's refusal. */
17
+ refuse(message: string): void;
18
+ /** A narrowed link, confirmed: run as the editor runs a command. */
19
+ command(action: FigureAuthoringAction): void;
20
+ }
21
+ /**
22
+ * Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
23
+ * notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
24
+ * delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
25
+ * other refusal is the app's notification, which stays until it is closed.
26
+ */
27
+ export declare class RefusalTeller {
28
+ private readonly of;
29
+ private toldViewError;
30
+ constructor(of: RefusalHost);
31
+ /** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */
32
+ tell(changed: PropertyValues): void;
33
+ askClipLoss(error: unknown, again: () => void): boolean;
34
+ private confirmClipLoss;
35
+ private ask;
36
+ }
@@ -0,0 +1,81 @@
1
+ import { i18next } from '@operato/i18n';
2
+ import { notify } from '@operato/layout';
3
+ import { OxPrompt } from '@operato/popup';
4
+ import { clipLossOf } from './refusals.js';
5
+ /**
6
+ * Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
7
+ * notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
8
+ * delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
9
+ * other refusal is the app's notification, which stays until it is closed.
10
+ */
11
+ export class RefusalTeller {
12
+ constructor(of) {
13
+ this.of = of;
14
+ this.toldViewError = '';
15
+ }
16
+ /** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */
17
+ tell(changed) {
18
+ const error = this.of.error();
19
+ if (changed.has('error') && error) {
20
+ const retry = this.of.retry();
21
+ if (retry)
22
+ this.ask(error, retry);
23
+ else
24
+ notify({ level: 'error', message: error });
25
+ }
26
+ const notice = this.of.notice();
27
+ if (changed.has('notice') && notice)
28
+ notify({ level: 'info', message: notice });
29
+ const viewError = this.of.session()?.read()?.error ?? '';
30
+ if (viewError && viewError !== this.toldViewError)
31
+ notify({ level: 'error', message: viewError });
32
+ this.toldViewError = viewError;
33
+ }
34
+ /*
35
+ An edit that would stop clips is refused by figure-model with the clips named (CLIPS_DROPPED). The person is told
36
+ which, and why, before anything is written; agreed, the same edit is made again letting exactly those go, as one
37
+ step undo brings back (designer 2026-09-27).
38
+ */
39
+ askClipLoss(error, again) {
40
+ const clips = clipLossOf(error);
41
+ if (!clips)
42
+ return false;
43
+ /* The consent is for this edit, on this figure as it is now: held with it while the person reads. */
44
+ const session = this.of.session();
45
+ const { context, revision } = session.read();
46
+ void this.confirmClipLoss(clips, again, { session, context: context, revision: revision });
47
+ return true;
48
+ }
49
+ async confirmClipLoss(clips, again, asked) {
50
+ const agreed = await OxPrompt.open({
51
+ type: 'warning',
52
+ text: i18next.t('figure.text.clips-would-go', { clips: clips.map(c => i18next.t(`figure.text.clip-${c.why}`, { clip: c.clip })).join(', ') }),
53
+ confirmButton: { text: i18next.t('figure.button.let-clips-go') },
54
+ cancelButton: { text: i18next.t('figure.button.keep-editing') }
55
+ });
56
+ if (!agreed)
57
+ return;
58
+ /* Changed while the person read -- another figure, or an edit in between: the consent was not for this; nothing is applied. */
59
+ const now = this.of.session() === asked.session ? asked.session.read() : undefined;
60
+ if (!now || now.context !== asked.context || now.revision !== asked.revision) {
61
+ this.of.refuse(i18next.t('figure.text.clips-consent-stale'));
62
+ return;
63
+ }
64
+ asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again);
65
+ }
66
+ async ask(message, retry) {
67
+ const agreed = await OxPrompt.open({
68
+ type: 'question',
69
+ text: message,
70
+ confirmButton: { text: retry.label },
71
+ cancelButton: { text: i18next.t('figure.button.keep-editing') }
72
+ });
73
+ if (!agreed)
74
+ return;
75
+ if (retry.run)
76
+ retry.run();
77
+ else
78
+ this.of.command(retry.action);
79
+ }
80
+ }
81
+ //# sourceMappingURL=refusal-teller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal-teller.js","sourceRoot":"","sources":["../../client/modeller/refusal-teller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { PropertyValues } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { OxPrompt } from '@operato/popup'\nimport type { FigureAuthoringAction, FigureClipLoss } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\nimport { clipLossOf } from './refusals.js'\n\n/** The same link offered again with the size range narrowed, after a link the declared range could not hold. */\nexport type Retry = { label: string; action?: FigureAuthoringAction; run?: () => void }\n\n/** What the teller reads of the editor, and what it asks of it. */\nexport interface RefusalHost {\n session(): FigureProductSession | undefined\n error(): string\n notice(): string\n retry(): Retry | undefined\n /** Said as the editor's refusal. */\n refuse(message: string): void\n /** A narrowed link, confirmed: run as the editor runs a command. */\n command(action: FigureAuthoringAction): void\n}\n\n/**\n * Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는\n * notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --\n * delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any\n * other refusal is the app's notification, which stays until it is closed.\n */\nexport class RefusalTeller {\n private toldViewError = ''\n\n constructor(private readonly of: RefusalHost) {}\n\n /** After the editor's update: what changed in its refusal and notice, and the session's own error, said once. */\n tell(changed: PropertyValues) {\n const error = this.of.error()\n if (changed.has('error') && error) {\n const retry = this.of.retry()\n if (retry) this.ask(error, retry)\n else notify({ level: 'error', message: error })\n }\n const notice = this.of.notice()\n if (changed.has('notice') && notice) notify({ level: 'info', message: notice })\n const viewError = this.of.session()?.read()?.error ?? ''\n if (viewError && viewError !== this.toldViewError) notify({ level: 'error', message: viewError })\n this.toldViewError = viewError\n }\n\n /*\n An edit that would stop clips is refused by figure-model with the clips named (CLIPS_DROPPED). The person is told\n which, and why, before anything is written; agreed, the same edit is made again letting exactly those go, as one\n step undo brings back (designer 2026-09-27).\n */\n askClipLoss(error: unknown, again: () => void): boolean {\n const clips = clipLossOf(error)\n if (!clips) return false\n /* The consent is for this edit, on this figure as it is now: held with it while the person reads. */\n const session = this.of.session()!\n const { context, revision } = session.read()\n void this.confirmClipLoss(clips, again, { session, context: context!, revision: revision! })\n return true\n }\n\n private async confirmClipLoss(clips: FigureClipLoss[], again: () => void, asked: { session: FigureProductSession; context: string; revision: number }) {\n const agreed = await OxPrompt.open({\n type: 'warning',\n text: i18next.t('figure.text.clips-would-go', { clips: clips.map(c => i18next.t(`figure.text.clip-${c.why}`, { clip: c.clip })).join(', ') }),\n confirmButton: { text: i18next.t('figure.button.let-clips-go') },\n cancelButton: { text: i18next.t('figure.button.keep-editing') }\n })\n if (!agreed) return\n /* Changed while the person read -- another figure, or an edit in between: the consent was not for this; nothing is applied. */\n const now = this.of.session() === asked.session ? asked.session.read() : undefined\n if (!now || now.context !== asked.context || now.revision !== asked.revision) {\n this.of.refuse(i18next.t('figure.text.clips-consent-stale'))\n return\n }\n asked.session.withConsent({ dropClips: clips.map(c => c.id), context: asked.context, revision: asked.revision }, again)\n }\n\n private async ask(message: string, retry: Retry) {\n const agreed = await OxPrompt.open({\n type: 'question',\n text: message,\n confirmButton: { text: retry.label },\n cancelButton: { text: i18next.t('figure.button.keep-editing') }\n })\n if (!agreed) return\n if (retry.run) retry.run()\n else this.of.command(retry.action!)\n }\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type FigureClipLoss, type FigureRefusalDetails } from '@hatiolab/figure-model';
1
2
  type Say = (key: string, params: Record<string, string>) => string;
2
3
  /** A size range the figure cannot be drawn over, or one that leaves out its own size, in words; nothing for any other refusal. */
3
4
  export declare function sizeRangeRefusalOf(error: unknown, say: Say): string | null;
@@ -19,6 +20,15 @@ export declare function narrowingRetryOf<A extends {
19
20
  * on the floor, when the figure is to hang on a wall), said in the author's words with the parts it names.
20
21
  */
21
22
  export declare function placementRefusalOf(error: unknown, say: Say): string | null;
23
+ /** A refusal carried on in a record (an AI proposal's outcome) in figure-model's own shape: its code and details. */
24
+ export interface FigureRefusalRecord {
25
+ code?: string;
26
+ details?: FigureRefusalDetails;
27
+ }
28
+ /** The record of a refusal: figure-model's code and details, copied as they are. */
29
+ export declare function refusalRecordOf(error: unknown): FigureRefusalRecord;
30
+ /** The clips an edit would stop, as figure-model named them (CLIPS_DROPPED) -- the person is asked before any go; nothing for any other refusal. */
31
+ export declare function clipLossOf(error: unknown): FigureClipLoss[] | null;
22
32
  /** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
23
33
  export declare function inDeclaredRoom(error: unknown): boolean;
24
34
  /**
@@ -70,6 +70,15 @@ export function placementRefusalOf(error, say) {
70
70
  faces: misfit.takes.join(', ')
71
71
  });
72
72
  }
73
+ /** The record of a refusal: figure-model's code and details, copied as they are. */
74
+ export function refusalRecordOf(error) {
75
+ const { details } = (error ?? {});
76
+ return { code: codeOf(error), ...(details ? { details } : {}) };
77
+ }
78
+ /** The clips an edit would stop, as figure-model named them (CLIPS_DROPPED) -- the person is asked before any go; nothing for any other refusal. */
79
+ export function clipLossOf(error) {
80
+ return codeOf(error) === 'CLIPS_DROPPED' ? (detailsOf(error, 'clips-dropped')?.clips ?? []) : null;
81
+ }
73
82
  /** A part refused because the declared room reads it -- the one refusal deleting it with the room refitted answers. */
74
83
  export function inDeclaredRoom(error) {
75
84
  return codeOf(error) === 'PART_REFERENCED' && !!detailsOf(error, 'in-declared-space');