@things-factory/figure-ui 10.1.77 → 10.1.78
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.
- package/client/modeller/editor.ts +42 -23
- package/dist-client/modeller/editor.d.ts +9 -0
- package/dist-client/modeller/editor.js +50 -24
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/test/browser/modeller.check.mjs +18 -12
- package/test/links.test.ts +1 -1
- package/test/parts-wiring.test.ts +13 -0
- package/test/refusals.test.ts +6 -3
- package/test/theme.test.ts +2 -4
- package/translations/en.json +2 -2
- package/translations/ja.json +2 -2
- package/translations/ko.json +2 -2
- package/translations/ms.json +2 -2
- package/translations/zh.json +2 -2
|
@@ -2,6 +2,8 @@ import { LitElement, html, css, nothing, type PropertyValues } from 'lit'
|
|
|
2
2
|
import { i18next } from '@operato/i18n'
|
|
3
3
|
import type { OxThreePane } from '@operato/layout'
|
|
4
4
|
import '@operato/layout'
|
|
5
|
+
import { notify } from '@operato/layout'
|
|
6
|
+
import { OxPrompt } from '@operato/popup'
|
|
5
7
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
6
8
|
import { live } from 'lit/directives/live.js'
|
|
7
9
|
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor } from '@hatiolab/figure-model'
|
|
@@ -247,17 +249,6 @@ export class FigureEditor extends LitElement {
|
|
|
247
249
|
color: var(--md-sys-color-on-secondary-container);
|
|
248
250
|
border-color: var(--md-sys-color-primary);
|
|
249
251
|
}
|
|
250
|
-
.error {
|
|
251
|
-
padding: 10px;
|
|
252
|
-
background: var(--md-sys-color-error-container);
|
|
253
|
-
color: var(--md-sys-color-on-error-container);
|
|
254
|
-
white-space: pre-wrap;
|
|
255
|
-
}
|
|
256
|
-
.notice {
|
|
257
|
-
padding: 8px 10px;
|
|
258
|
-
background: var(--md-sys-color-surface-container);
|
|
259
|
-
color: var(--md-sys-color-on-surface);
|
|
260
|
-
}
|
|
261
252
|
.hint {
|
|
262
253
|
color: var(--md-sys-color-on-surface-variant);
|
|
263
254
|
line-height: 1.5;
|
|
@@ -386,6 +377,35 @@ export class FigureEditor extends LitElement {
|
|
|
386
377
|
window.removeEventListener('keydown', this.onKey, true)
|
|
387
378
|
super.disconnectedCallback()
|
|
388
379
|
}
|
|
380
|
+
/**
|
|
381
|
+
* Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
|
|
382
|
+
* notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
|
|
383
|
+
* delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
|
|
384
|
+
* other refusal is the app's notification, which stays until it is closed.
|
|
385
|
+
*/
|
|
386
|
+
private toldViewError = ''
|
|
387
|
+
private tell(changed: PropertyValues) {
|
|
388
|
+
if (changed.has('error') && this.error) {
|
|
389
|
+
const retry = this.retry
|
|
390
|
+
if (retry) this.ask(this.error, retry)
|
|
391
|
+
else notify({ level: 'error', message: this.error })
|
|
392
|
+
}
|
|
393
|
+
if (changed.has('notice') && this.notice) notify({ level: 'info', message: this.notice })
|
|
394
|
+
const viewError = this.session?.read()?.error ?? ''
|
|
395
|
+
if (viewError && viewError !== this.toldViewError) notify({ level: 'error', message: viewError })
|
|
396
|
+
this.toldViewError = viewError
|
|
397
|
+
}
|
|
398
|
+
private async ask(message: string, retry: { label: string; action?: FigureAuthoringAction; run?: () => void }) {
|
|
399
|
+
const agreed = await OxPrompt.open({
|
|
400
|
+
type: 'question',
|
|
401
|
+
text: message,
|
|
402
|
+
confirmButton: { text: retry.label },
|
|
403
|
+
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
404
|
+
})
|
|
405
|
+
if (!agreed) return
|
|
406
|
+
if (retry.run) retry.run()
|
|
407
|
+
else this.linkCommand(retry.action!)
|
|
408
|
+
}
|
|
389
409
|
protected willUpdate(_changed: PropertyValues) {
|
|
390
410
|
const view = this.session?.read()
|
|
391
411
|
if (view?.context !== this.lastContext) {
|
|
@@ -524,10 +544,17 @@ export class FigureEditor extends LitElement {
|
|
|
524
544
|
if (this.selected === id) this.selected = ''
|
|
525
545
|
this.changed()
|
|
526
546
|
} catch (error) {
|
|
527
|
-
/*
|
|
547
|
+
/*
|
|
548
|
+
A part in the declared space goes, and the space is fitted to the parts left, in the same click: every space
|
|
549
|
+
this product declares is fitted to the parts (「부품에 맞추기」, the AI's fit-room -- no padding), so fitting it
|
|
550
|
+
again keeps what it says, and there is nothing to ask (user, 2026-09-27: 「이 확인이 꼭 필요한가 ?」).
|
|
551
|
+
*/
|
|
552
|
+
if (!refitRoom && inDeclaredRoom(error)) {
|
|
553
|
+
this.removePart(id, true)
|
|
554
|
+
if (!this.error) this.notice = [this.notice, i18next.t('figure.text.removal-space-refitted')].filter(Boolean).join(' ')
|
|
555
|
+
return
|
|
556
|
+
}
|
|
528
557
|
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error)
|
|
529
|
-
/* In the declared room only: the way on is to delete it and fit the room to the parts left, in one step. */
|
|
530
|
-
if (!refitRoom && inDeclaredRoom(error)) this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) }
|
|
531
558
|
}
|
|
532
559
|
}
|
|
533
560
|
private toggleHiddenPart(id: string) {
|
|
@@ -1186,6 +1213,7 @@ export class FigureEditor extends LitElement {
|
|
|
1186
1213
|
}
|
|
1187
1214
|
updated(changed: PropertyValues<this>) {
|
|
1188
1215
|
super.updated?.(changed)
|
|
1216
|
+
this.tell(changed)
|
|
1189
1217
|
const panes = this.renderRoot.querySelector<OxThreePane>('ox-three-pane') ?? undefined
|
|
1190
1218
|
if (panes !== this.panes) this.panes = panes
|
|
1191
1219
|
}
|
|
@@ -1238,15 +1266,6 @@ export class FigureEditor extends LitElement {
|
|
|
1238
1266
|
<span slot="status" class="status" role="status" ?dirty=${status === 'figure.text.unsaved-changes'}>${i18next.t(status)}</span>
|
|
1239
1267
|
<button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>${i18next.t('figure.button.save')}</button>
|
|
1240
1268
|
</ox-canvas-bar>
|
|
1241
|
-
${this.error || view.error
|
|
1242
|
-
? html`<div role="alert" class="error">
|
|
1243
|
-
${this.error || view.error}
|
|
1244
|
-
${this.retry && this.error
|
|
1245
|
-
? html`<button act @click=${() => (this.retry!.run ? this.retry!.run() : this.linkCommand(this.retry!.action!))}><md-icon>straighten</md-icon>${this.retry.label}</button>`
|
|
1246
|
-
: null}
|
|
1247
|
-
</div>`
|
|
1248
|
-
: null}
|
|
1249
|
-
${this.notice ? html`<div role="status" class="notice">${this.notice}</div>` : null}
|
|
1250
1269
|
${this.catalogOpen
|
|
1251
1270
|
? html`<figure-recipe-catalog
|
|
1252
1271
|
.templates=${this.templates}
|
|
@@ -76,6 +76,15 @@ export declare class FigureEditor extends LitElement {
|
|
|
76
76
|
private onKey;
|
|
77
77
|
connectedCallback(): void;
|
|
78
78
|
disconnectedCallback(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
|
|
81
|
+
* notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
|
|
82
|
+
* delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
|
|
83
|
+
* other refusal is the app's notification, which stays until it is closed.
|
|
84
|
+
*/
|
|
85
|
+
private toldViewError;
|
|
86
|
+
private tell;
|
|
87
|
+
private ask;
|
|
79
88
|
protected willUpdate(_changed: PropertyValues): void;
|
|
80
89
|
/** The scene toggles: the mode already shown takes the gizmo away. */
|
|
81
90
|
private useGizmo;
|
|
@@ -3,6 +3,8 @@ import { __decorate, __metadata } from "tslib";
|
|
|
3
3
|
import { LitElement, html, css, nothing } from 'lit';
|
|
4
4
|
import { i18next } from '@operato/i18n';
|
|
5
5
|
import '@operato/layout';
|
|
6
|
+
import { notify } from '@operato/layout';
|
|
7
|
+
import { OxPrompt } from '@operato/popup';
|
|
6
8
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
7
9
|
import { live } from 'lit/directives/live.js';
|
|
8
10
|
import { PART_LIMIT, parseFigureRecipe, resolveFigureRecipe, figurePartCountOf, figureRecipeIdFor } from '@hatiolab/figure-model';
|
|
@@ -92,6 +94,13 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
92
94
|
else
|
|
93
95
|
this.selected = '';
|
|
94
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
|
|
99
|
+
* notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
|
|
100
|
+
* delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
|
|
101
|
+
* other refusal is the app's notification, which stays until it is closed.
|
|
102
|
+
*/
|
|
103
|
+
this.toldViewError = '';
|
|
95
104
|
}
|
|
96
105
|
static { FigureEditor_1 = this; }
|
|
97
106
|
/* The operato scrollbar, as every scrolling panel of the app has it -- the side panel scrolled with the browser's own. */
|
|
@@ -291,17 +300,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
291
300
|
color: var(--md-sys-color-on-secondary-container);
|
|
292
301
|
border-color: var(--md-sys-color-primary);
|
|
293
302
|
}
|
|
294
|
-
.error {
|
|
295
|
-
padding: 10px;
|
|
296
|
-
background: var(--md-sys-color-error-container);
|
|
297
|
-
color: var(--md-sys-color-on-error-container);
|
|
298
|
-
white-space: pre-wrap;
|
|
299
|
-
}
|
|
300
|
-
.notice {
|
|
301
|
-
padding: 8px 10px;
|
|
302
|
-
background: var(--md-sys-color-surface-container);
|
|
303
|
-
color: var(--md-sys-color-on-surface);
|
|
304
|
-
}
|
|
305
303
|
.hint {
|
|
306
304
|
color: var(--md-sys-color-on-surface-variant);
|
|
307
305
|
line-height: 1.5;
|
|
@@ -358,6 +356,35 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
358
356
|
window.removeEventListener('keydown', this.onKey, true);
|
|
359
357
|
super.disconnectedCallback();
|
|
360
358
|
}
|
|
359
|
+
tell(changed) {
|
|
360
|
+
if (changed.has('error') && this.error) {
|
|
361
|
+
const retry = this.retry;
|
|
362
|
+
if (retry)
|
|
363
|
+
this.ask(this.error, retry);
|
|
364
|
+
else
|
|
365
|
+
notify({ level: 'error', message: this.error });
|
|
366
|
+
}
|
|
367
|
+
if (changed.has('notice') && this.notice)
|
|
368
|
+
notify({ level: 'info', message: this.notice });
|
|
369
|
+
const viewError = this.session?.read()?.error ?? '';
|
|
370
|
+
if (viewError && viewError !== this.toldViewError)
|
|
371
|
+
notify({ level: 'error', message: viewError });
|
|
372
|
+
this.toldViewError = viewError;
|
|
373
|
+
}
|
|
374
|
+
async ask(message, retry) {
|
|
375
|
+
const agreed = await OxPrompt.open({
|
|
376
|
+
type: 'question',
|
|
377
|
+
text: message,
|
|
378
|
+
confirmButton: { text: retry.label },
|
|
379
|
+
cancelButton: { text: i18next.t('figure.button.keep-editing') }
|
|
380
|
+
});
|
|
381
|
+
if (!agreed)
|
|
382
|
+
return;
|
|
383
|
+
if (retry.run)
|
|
384
|
+
retry.run();
|
|
385
|
+
else
|
|
386
|
+
this.linkCommand(retry.action);
|
|
387
|
+
}
|
|
361
388
|
willUpdate(_changed) {
|
|
362
389
|
const view = this.session?.read();
|
|
363
390
|
if (view?.context !== this.lastContext) {
|
|
@@ -510,11 +537,18 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
510
537
|
this.changed();
|
|
511
538
|
}
|
|
512
539
|
catch (error) {
|
|
513
|
-
/*
|
|
540
|
+
/*
|
|
541
|
+
A part in the declared space goes, and the space is fitted to the parts left, in the same click: every space
|
|
542
|
+
this product declares is fitted to the parts (「부품에 맞추기」, the AI's fit-room -- no padding), so fitting it
|
|
543
|
+
again keeps what it says, and there is nothing to ask (user, 2026-09-27: 「이 확인이 꼭 필요한가 ?」).
|
|
544
|
+
*/
|
|
545
|
+
if (!refitRoom && inDeclaredRoom(error)) {
|
|
546
|
+
this.removePart(id, true);
|
|
547
|
+
if (!this.error)
|
|
548
|
+
this.notice = [this.notice, i18next.t('figure.text.removal-space-refitted')].filter(Boolean).join(' ');
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
514
551
|
this.error = partRefusalOf(error, id, (key, params) => i18next.t(key, params)) ?? this.said(error);
|
|
515
|
-
/* In the declared room only: the way on is to delete it and fit the room to the parts left, in one step. */
|
|
516
|
-
if (!refitRoom && inDeclaredRoom(error))
|
|
517
|
-
this.retry = { label: i18next.t('figure.button.remove-and-refit-room', { part: id }), run: () => this.removePart(id, true) };
|
|
518
552
|
}
|
|
519
553
|
}
|
|
520
554
|
toggleHiddenPart(id) {
|
|
@@ -1198,6 +1232,7 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1198
1232
|
}
|
|
1199
1233
|
updated(changed) {
|
|
1200
1234
|
super.updated?.(changed);
|
|
1235
|
+
this.tell(changed);
|
|
1201
1236
|
const panes = this.renderRoot.querySelector('ox-three-pane') ?? undefined;
|
|
1202
1237
|
if (panes !== this.panes)
|
|
1203
1238
|
this.panes = panes;
|
|
@@ -1251,15 +1286,6 @@ let FigureEditor = class FigureEditor extends LitElement {
|
|
|
1251
1286
|
<span slot="status" class="status" role="status" ?dirty=${status === 'figure.text.unsaved-changes'}>${i18next.t(status)}</span>
|
|
1252
1287
|
<button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>${i18next.t('figure.button.save')}</button>
|
|
1253
1288
|
</ox-canvas-bar>
|
|
1254
|
-
${this.error || view.error
|
|
1255
|
-
? html `<div role="alert" class="error">
|
|
1256
|
-
${this.error || view.error}
|
|
1257
|
-
${this.retry && this.error
|
|
1258
|
-
? html `<button act @click=${() => (this.retry.run ? this.retry.run() : this.linkCommand(this.retry.action))}><md-icon>straighten</md-icon>${this.retry.label}</button>`
|
|
1259
|
-
: null}
|
|
1260
|
-
</div>`
|
|
1261
|
-
: null}
|
|
1262
|
-
${this.notice ? html `<div role="status" class="notice">${this.notice}</div>` : null}
|
|
1263
1289
|
${this.catalogOpen
|
|
1264
1290
|
? html `<figure-recipe-catalog
|
|
1265
1291
|
.templates=${this.templates}
|