@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
@@ -0,0 +1,178 @@
1
+ import { i18next } from '@operato/i18n';
2
+ import { figurePartFaceOf, figureRowOf } from '@hatiolab/figure-model';
3
+ import { faceHighlightOf, ghostPartsOf, previewWordsOf } from './command-preview.js';
4
+ import { partLabelOf } from './fields.js';
5
+ import { derivedHeld, derivedOf } from './revision.js';
6
+ /** The words a panel is given when there are none: one array, so it does not read as a change. */
7
+ export const NO_WORDS = [];
8
+ /** How long a command's candidate rests before its preview is worked out. */
9
+ const PREVIEW_SETTLE_MS = 150;
10
+ /**
11
+ * A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it
12
+ * names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone. And what taking
13
+ * the selected part off would do, worked out the same way. Both wait for the figure to rest; the editor taken down
14
+ * stops both.
15
+ */
16
+ export class CommandPreview {
17
+ constructor(host, of) {
18
+ this.host = host;
19
+ this.of = of;
20
+ host.addController(this);
21
+ }
22
+ hostConnected() { }
23
+ hostDisconnected() {
24
+ clearTimeout(this.previewTimer);
25
+ clearTimeout(this.detachTimer);
26
+ }
27
+ /** Drawn again only when it is another preview, as a state of the editor would be. */
28
+ hold(preview) {
29
+ if (preview === this.preview)
30
+ return;
31
+ this.preview = preview;
32
+ this.host.requestUpdate();
33
+ }
34
+ /**
35
+ * The fastening form's command, seen before it is done; null clears what was shown. What a command would do is worked
36
+ * out on a copy of the figure -- several compiles of it -- so it waits for the field to rest: a number typed digit by
37
+ * digit asks once, for the number it ends on. Clearing is at once, and the same question on the same version is not
38
+ * asked again.
39
+ */
40
+ ask(candidate) {
41
+ clearTimeout(this.previewTimer);
42
+ const session = this.of.session();
43
+ const selected = this.of.selected();
44
+ if (!candidate || !session || !selected) {
45
+ this.hold(undefined);
46
+ return;
47
+ }
48
+ const view = session.read();
49
+ const key = JSON.stringify(candidate);
50
+ const same = this.preview &&
51
+ this.preview.key === key &&
52
+ this.preview.context === view.context &&
53
+ this.preview.part === selected &&
54
+ 'revision' in this.preview &&
55
+ this.preview.revision === view.revision;
56
+ if (same)
57
+ return;
58
+ /*
59
+ What another command would do is not said of this one while this one is worked out: a row's pitch refused stayed
60
+ on screen for the count chosen after it (:3300, 2026-10-11).
61
+ */
62
+ if (this.preview && this.preview.key !== key)
63
+ this.hold(undefined);
64
+ this.previewTimer = setTimeout(() => this.compute(candidate), PREVIEW_SETTLE_MS);
65
+ }
66
+ compute(candidate) {
67
+ const session = this.of.session();
68
+ const selected = this.of.selected();
69
+ if (!session || !selected)
70
+ return;
71
+ const view = session.read();
72
+ const held = { context: view.context, revision: view.revision, key: JSON.stringify(candidate), part: selected };
73
+ try {
74
+ const seen = session.previewCommand(candidate.action);
75
+ let face;
76
+ if (candidate.face) {
77
+ try {
78
+ const shown = this.of.shown();
79
+ face = figurePartFaceOf(shown.asset, candidate.face.part, candidate.face.face, shown.inputs);
80
+ }
81
+ catch {
82
+ face = undefined;
83
+ }
84
+ }
85
+ /* A row is read on the figure the command would make, by figure-model, as drawn. */
86
+ const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, selected, this.of.shown().inputs) : undefined;
87
+ this.hold({
88
+ ...held,
89
+ context: seen.context,
90
+ revision: seen.revision,
91
+ geometry: seen.geometry,
92
+ moves: seen.moves,
93
+ added: seen.added,
94
+ ...(row ? { row } : {}),
95
+ clipsWouldGo: seen.clipsWouldGo,
96
+ face,
97
+ faceUnknown: !!candidate.face && !face
98
+ });
99
+ }
100
+ catch (error) {
101
+ /* Refused: nothing of an earlier result stays on screen, and the reason is said. */
102
+ this.hold({ ...held, refused: this.of.said(error) });
103
+ }
104
+ }
105
+ /** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
106
+ current() {
107
+ const p = this.preview;
108
+ const view = this.of.session()?.read();
109
+ /* Only where its form is: the properties panel stays in the page on another tab, and did not clear it (2026-09-27, :3391). */
110
+ if (!p || !view || p.context !== view.context || p.revision !== view.revision || p.part !== this.of.selected() || !this.of.formsInSight())
111
+ return undefined;
112
+ return p;
113
+ }
114
+ /** What the preview says, the same array while it says the same thing. */
115
+ words() {
116
+ const preview = this.current();
117
+ const selected = this.of.selected();
118
+ const language = i18next.language;
119
+ if (this.wordsSaid && this.wordsSaid.preview === preview && this.wordsSaid.part === selected && this.wordsSaid.language === language)
120
+ return this.wordsSaid.words;
121
+ const words = previewWordsOf(preview, selected, (key, params) => i18next.t(key, params));
122
+ this.wordsSaid = { preview, part: selected, language, words };
123
+ return words;
124
+ }
125
+ /** What the canvas draws of a preview, made once per preview: a new list every render would stand the board up each time. */
126
+ drawnOf(p) {
127
+ if (!p || p.refused)
128
+ return { ghost: [], highlight: null };
129
+ if (this.drawn?.of !== p)
130
+ this.drawn = { of: p, ghost: ghostPartsOf(p), highlight: faceHighlightOf(p.face) };
131
+ return this.drawn;
132
+ }
133
+ /**
134
+ * Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
135
+ * edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
136
+ */
137
+ detachWords(shown) {
138
+ const part = this.of.selected();
139
+ if (!this.of.session() || !shown.asset.joints?.some(j => j.body1.part === part))
140
+ return NO_WORDS;
141
+ const key = `detach|${part}`;
142
+ const said = derivedHeld(shown, key);
143
+ if (said)
144
+ return said;
145
+ if (this.detachAsked?.shown !== shown || this.detachAsked.part !== part) {
146
+ this.detachAsked = { shown, part };
147
+ clearTimeout(this.detachTimer);
148
+ this.detachTimer = setTimeout(() => {
149
+ if (this.of.shown() !== shown || this.of.selected() !== part)
150
+ return;
151
+ derivedOf(shown, key, () => this.computeDetachWords(shown, part));
152
+ this.host.requestUpdate();
153
+ }, PREVIEW_SETTLE_MS);
154
+ }
155
+ return NO_WORDS;
156
+ }
157
+ computeDetachWords(shown, part) {
158
+ const say = (k, params) => i18next.t(k, params);
159
+ let words;
160
+ try {
161
+ const seen = this.of.session().previewCommand({ kind: 'detach', part });
162
+ const own = seen.moves.some(m => m.body === part);
163
+ words = own
164
+ ? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key: `detach|${part}`, moves: seen.moves }, part, say)]
165
+ : /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ [];
166
+ const children = (shown.asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => partLabelOf(j.body1.part));
167
+ if (children.length)
168
+ words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }));
169
+ if (seen.clipsWouldGo.length)
170
+ words.push(say('figure.text.detach-clips', { clips: seen.clipsWouldGo.map(c => c.clip).join(', ') }));
171
+ }
172
+ catch (error) {
173
+ words = [say('figure.text.detach-refused', { reason: this.of.said(error) })];
174
+ }
175
+ return words;
176
+ }
177
+ }
178
+ //# sourceMappingURL=command-preview-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-preview-controller.js","sourceRoot":"","sources":["../../client/modeller/command-preview-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit'\nimport { i18next } from '@operato/i18n'\nimport { figurePartFaceOf, figureRowOf, type FigureAuthoringAction, type FigureFaceArea } from '@hatiolab/figure-model'\nimport { faceHighlightOf, ghostPartsOf, previewWordsOf, type HeldPreview } from './command-preview.js'\nimport { partLabelOf } from './fields.js'\nimport type { FigureProductSession } from './product-session.js'\nimport type { FigureRenderPart } from './renderer.js'\nimport { derivedHeld, derivedOf, type FigureRevision } from './revision.js'\n\n/** The words a panel is given when there are none: one array, so it does not read as a change. */\nexport const NO_WORDS: string[] = []\n\n/** How long a command's candidate rests before its preview is worked out. */\nconst PREVIEW_SETTLE_MS = 150\n\n/** A command a form is trying, with the face it names when it names one. */\nexport type PreviewCandidate = { action: FigureAuthoringAction; face?: { part: string; face: string } }\n\n/** What the preview reads of the editor. */\nexport interface CommandPreviewHost {\n session(): FigureProductSession | undefined\n selected(): string\n /** The figure on screen at its version (`revision.ts`). */\n shown(): FigureRevision | undefined\n /** Whether the properties panel, where the forms are, is in sight: the edit middle, the properties tab. */\n formsInSight(): boolean\n /** A refusal in the person's words. */\n said(error: unknown): string\n}\n\n/**\n * A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it\n * names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone. And what taking\n * the selected part off would do, worked out the same way. Both wait for the figure to rest; the editor taken down\n * stops both.\n */\nexport class CommandPreview implements ReactiveController {\n private preview?: HeldPreview & { part: string }\n private drawn?: { of: HeldPreview; ghost: FigureRenderPart[]; highlight: FigureRenderPart | null }\n /*\n What a panel is handed is the same object while it says the same thing: a new array each render reads as a change to\n the panel, which draws itself again for nothing (sixteen times for one edit).\n */\n private wordsSaid?: { preview: unknown; part: string; language: string; words: string[] }\n private previewTimer?: ReturnType<typeof setTimeout>\n /*\n What taking the selected part off would do, worked out on a copy before the button is pressed (ADR-0097 §5.5): whether\n it stays where it stands now, what stays fastened to it, which clips would go. \"It stays where it is\" is said only\n when the copy shows it does.\n */\n private detachTimer?: ReturnType<typeof setTimeout>\n private detachAsked?: { shown: FigureRevision; part: string }\n\n constructor(\n private readonly host: ReactiveControllerHost,\n private readonly of: CommandPreviewHost\n ) {\n host.addController(this)\n }\n\n hostConnected() {}\n\n hostDisconnected() {\n clearTimeout(this.previewTimer)\n clearTimeout(this.detachTimer)\n }\n\n /** Drawn again only when it is another preview, as a state of the editor would be. */\n private hold(preview: (HeldPreview & { part: string }) | undefined) {\n if (preview === this.preview) return\n this.preview = preview\n this.host.requestUpdate()\n }\n\n /**\n * The fastening form's command, seen before it is done; null clears what was shown. What a command would do is worked\n * out on a copy of the figure -- several compiles of it -- so it waits for the field to rest: a number typed digit by\n * digit asks once, for the number it ends on. Clearing is at once, and the same question on the same version is not\n * asked again.\n */\n ask(candidate: PreviewCandidate | null) {\n clearTimeout(this.previewTimer)\n const session = this.of.session()\n const selected = this.of.selected()\n if (!candidate || !session || !selected) {\n this.hold(undefined)\n return\n }\n const view = session.read()\n const key = JSON.stringify(candidate)\n const same =\n this.preview &&\n this.preview.key === key &&\n this.preview.context === view.context &&\n this.preview.part === selected &&\n 'revision' in this.preview &&\n this.preview.revision === view.revision\n if (same) return\n /*\n What another command would do is not said of this one while this one is worked out: a row's pitch refused stayed\n on screen for the count chosen after it (:3300, 2026-10-11).\n */\n if (this.preview && this.preview.key !== key) this.hold(undefined)\n this.previewTimer = setTimeout(() => this.compute(candidate), PREVIEW_SETTLE_MS)\n }\n\n private compute(candidate: PreviewCandidate) {\n const session = this.of.session()\n const selected = this.of.selected()\n if (!session || !selected) return\n const view = session.read()\n const held = { context: view.context!, revision: view.revision!, key: JSON.stringify(candidate), part: selected }\n try {\n const seen = session.previewCommand(candidate.action)\n let face: FigureFaceArea | undefined\n if (candidate.face) {\n try {\n const shown = this.of.shown()!\n face = figurePartFaceOf(shown.asset, candidate.face.part, candidate.face.face, shown.inputs)\n } catch {\n face = undefined\n }\n }\n /* A row is read on the figure the command would make, by figure-model, as drawn. */\n const row = candidate.action.kind === 'make-repeat' ? figureRowOf(seen.asset, selected, this.of.shown()!.inputs) : undefined\n this.hold({\n ...held,\n context: seen.context,\n revision: seen.revision,\n geometry: seen.geometry,\n moves: seen.moves,\n added: seen.added,\n ...(row ? { row } : {}),\n clipsWouldGo: seen.clipsWouldGo,\n face,\n faceUnknown: !!candidate.face && !face\n })\n } catch (error) {\n /* Refused: nothing of an earlier result stays on screen, and the reason is said. */\n this.hold({ ...held, refused: this.of.said(error) })\n }\n }\n\n /** The preview, while it still speaks of what is on screen: this figure, this version, this part. */\n current(): (HeldPreview & { part: string }) | undefined {\n const p = this.preview\n const view = this.of.session()?.read()\n /* Only where its form is: the properties panel stays in the page on another tab, and did not clear it (2026-09-27, :3391). */\n if (!p || !view || p.context !== view.context || p.revision !== view.revision || p.part !== this.of.selected() || !this.of.formsInSight()) return undefined\n return p\n }\n\n /** What the preview says, the same array while it says the same thing. */\n words(): string[] {\n const preview = this.current()\n const selected = this.of.selected()\n const language = i18next.language\n if (this.wordsSaid && this.wordsSaid.preview === preview && this.wordsSaid.part === selected && this.wordsSaid.language === language) return this.wordsSaid.words\n const words = previewWordsOf(preview, selected, (key, params) => i18next.t(key, params))\n this.wordsSaid = { preview, part: selected, language, words }\n return words\n }\n\n /** What the canvas draws of a preview, made once per preview: a new list every render would stand the board up each time. */\n drawnOf(p: (HeldPreview & { part: string }) | undefined): { ghost: FigureRenderPart[]; highlight: FigureRenderPart | null } {\n if (!p || p.refused) return { ghost: [], highlight: null }\n if (this.drawn?.of !== p) this.drawn = { of: p, ghost: ghostPartsOf(p), highlight: faceHighlightOf(p.face) }\n return this.drawn\n }\n\n /**\n * Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the\n * edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.\n */\n detachWords(shown: FigureRevision): string[] {\n const part = this.of.selected()\n if (!this.of.session() || !shown.asset.joints?.some(j => j.body1.part === part)) return NO_WORDS\n const key = `detach|${part}`\n const said = derivedHeld<string[]>(shown, key)\n if (said) return said\n if (this.detachAsked?.shown !== shown || this.detachAsked.part !== part) {\n this.detachAsked = { shown, part }\n clearTimeout(this.detachTimer)\n this.detachTimer = setTimeout(() => {\n if (this.of.shown() !== shown || this.of.selected() !== part) return\n derivedOf(shown, key, () => this.computeDetachWords(shown, part))\n this.host.requestUpdate()\n }, PREVIEW_SETTLE_MS)\n }\n return NO_WORDS\n }\n\n private computeDetachWords(shown: FigureRevision, part: string): string[] {\n const say = (k: string, params?: Record<string, string>) => i18next.t(k, params)\n let words: string[]\n try {\n const seen = this.of.session()!.previewCommand({ kind: 'detach', part })\n const own = seen.moves.some(m => m.body === part)\n words = own\n ? [say('figure.text.detach-moves'), ...previewWordsOf({ context: seen.context, revision: seen.revision, key: `detach|${part}`, moves: seen.moves }, part, say)]\n : /* It stays where it is: as expected, so nothing is said (user 2026-10-01). */ []\n const children = (shown.asset.joints ?? []).filter(j => 'part' in j.body0 && j.body0.part === part).map(j => partLabelOf(j.body1.part))\n if (children.length) words.push(say('figure.text.detach-keeps-children', { parts: children.join(', ') }))\n if (seen.clipsWouldGo.length) words.push(say('figure.text.detach-clips', { clips: seen.clipsWouldGo.map(c => c.clip).join(', ') }))\n } catch (error) {\n words = [say('figure.text.detach-refused', { reason: this.of.said(error) })]\n }\n return words\n }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ /** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */
2
+ export declare const EditorStyles: import("lit").CSSResult;
@@ -0,0 +1,307 @@
1
+ import { css } from 'lit';
2
+ /** The modeller editor's own styles: the bar's tools, the panes, the start card, the side's tabs and panels. */
3
+ export const EditorStyles = css `
4
+ :host {
5
+ display: flex;
6
+ flex-direction: column;
7
+ min-height: 0;
8
+ height: 100%;
9
+ /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */
10
+ container-type: inline-size;
11
+ font: 13px sans-serif;
12
+ color: var(--md-sys-color-on-surface);
13
+ }
14
+ /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and
15
+ fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */
16
+ ox-canvas-bar button[mode] {
17
+ height: 28px;
18
+ padding: 0 10px;
19
+ border-radius: 999em;
20
+ border-color: transparent;
21
+ background: none;
22
+ }
23
+ ox-canvas-bar button[mode][aria-pressed='true'] {
24
+ border-color: var(--md-sys-color-primary);
25
+ }
26
+ /* The gizmo's tools, drawn as the bar's own icon buttons (undo, redo, fit). */
27
+ ox-canvas-bar .gizmo-tools {
28
+ display: inline-flex;
29
+ gap: 2px;
30
+ padding-left: 4px;
31
+ border-left: 1px solid var(--md-sys-color-outline-variant);
32
+ }
33
+ ox-canvas-bar button[tool] {
34
+ display: inline-grid;
35
+ place-items: center;
36
+ width: 32px;
37
+ height: 32px;
38
+ padding: 0;
39
+ border: none;
40
+ border-radius: var(--md-sys-shape-corner-small, 8px);
41
+ background: transparent;
42
+ color: var(--md-sys-color-on-surface-variant);
43
+ cursor: pointer;
44
+ }
45
+ ox-canvas-bar button[tool]:hover:not(:disabled) {
46
+ background: var(--md-sys-color-surface-container-high);
47
+ color: var(--md-sys-color-on-surface);
48
+ }
49
+ ox-canvas-bar button[tool][aria-pressed='true'] {
50
+ background: var(--md-sys-color-secondary-container);
51
+ color: var(--md-sys-color-on-secondary-container);
52
+ }
53
+ ox-canvas-bar button[tool]:disabled {
54
+ opacity: 0.38;
55
+ cursor: default;
56
+ }
57
+ /*
58
+ Slotted into the bar from here, the tools do not get the bar's own icon size (20px, set inside its shadow): they
59
+ showed at md-icon's 24px, heavier than undo, redo and fit beside them. The same size, and the bar's focus ring.
60
+ */
61
+ ox-canvas-bar button[tool] md-icon {
62
+ --md-icon-size: 20px;
63
+ }
64
+ ox-canvas-bar button[tool]:focus-visible {
65
+ outline: 2px solid var(--md-sys-color-primary);
66
+ outline-offset: 1px;
67
+ }
68
+ figure-preview {
69
+ min-width: 0;
70
+ min-height: 0;
71
+ }
72
+ /* 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. */
73
+ div[middle][hidden] {
74
+ display: none;
75
+ }
76
+ /* The preview, once made, stays standing out of sight too: its 3D scene is not made again each time it is asked for. */
77
+ figure-preview[hidden] {
78
+ display: none;
79
+ }
80
+ ox-canvas-bar .status {
81
+ padding: 0 8px;
82
+ color: var(--md-sys-color-on-surface-variant);
83
+ }
84
+ ox-canvas-bar .status[dirty] {
85
+ color: var(--md-sys-color-primary);
86
+ font-weight: 600;
87
+ }
88
+ ox-canvas-bar button[save] {
89
+ height: 32px;
90
+ padding: 0 14px;
91
+ border: none;
92
+ border-radius: 999em;
93
+ color: var(--md-sys-color-on-primary);
94
+ background: var(--md-sys-color-primary);
95
+ }
96
+ /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under
97
+ 860px and one column with the middle first under 560px are the frame's, as are the dividers. */
98
+ ox-three-pane {
99
+ flex: 1;
100
+ min-height: 0;
101
+ }
102
+ figure-parts {
103
+ min-height: 0;
104
+ }
105
+ figure-canvas {
106
+ flex: 1;
107
+ min-width: 0;
108
+ min-height: 0;
109
+ }
110
+ div[middle] {
111
+ position: relative;
112
+ display: flex;
113
+ min-width: 0;
114
+ min-height: 0;
115
+ }
116
+ /* A new figure's first question, over the empty canvas. */
117
+ div[start] {
118
+ position: absolute;
119
+ inset: 50% auto auto 50%;
120
+ z-index: 10;
121
+ color: var(--md-sys-color-on-surface);
122
+ transform: translate(-50%, -50%);
123
+ width: min(440px, calc(100% - 32px));
124
+ box-sizing: border-box;
125
+ padding: 20px;
126
+ border-radius: 12px;
127
+ background: var(--md-sys-color-surface, #fff);
128
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
129
+ }
130
+ div[start] button[start-button] {
131
+ width: auto;
132
+ margin: 0;
133
+ font-size: var(--ui-t-body);
134
+ height: 36px;
135
+ padding: 0 16px;
136
+ border: 1px solid var(--md-sys-color-outline);
137
+ border-radius: 999em;
138
+ color: var(--md-sys-color-primary);
139
+ background: var(--md-sys-color-surface, #fff);
140
+ cursor: pointer;
141
+ }
142
+ div[start] button[start-button]:hover:not(:disabled) {
143
+ background: var(--md-sys-color-primary-container);
144
+ }
145
+ div[start] button[start-button][pressed] {
146
+ color: var(--md-sys-color-on-primary);
147
+ background: var(--md-sys-color-primary);
148
+ border-color: var(--md-sys-color-primary);
149
+ }
150
+ div[start] button[start-button]:disabled {
151
+ opacity: 0.5;
152
+ cursor: default;
153
+ }
154
+ div[start] h2 {
155
+ margin: 0 0 12px;
156
+ font-size: var(--ui-t-title);
157
+ }
158
+ div[start-paths] {
159
+ display: grid;
160
+ gap: 14px;
161
+ }
162
+ div[start-paths] h3 {
163
+ margin: 0 0 6px;
164
+ font-size: var(--ui-t-body);
165
+ }
166
+ div[start-shapes] {
167
+ display: flex;
168
+ gap: 8px;
169
+ flex-wrap: wrap;
170
+ }
171
+ div[start] small {
172
+ display: block;
173
+ margin-top: 4px;
174
+ color: var(--md-sys-color-on-surface-variant);
175
+ }
176
+ /* The side: tabs over one panel at a time, each using the whole height. */
177
+ aside {
178
+ display: flex;
179
+ flex-direction: column;
180
+ min-height: 0;
181
+ background: var(--md-sys-color-surface-container-lowest);
182
+ }
183
+ div[tabs] {
184
+ display: flex;
185
+ flex: none;
186
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
187
+ }
188
+ button[tab] {
189
+ flex: 1;
190
+ position: relative;
191
+ display: flex;
192
+ flex-direction: column;
193
+ align-items: center;
194
+ justify-content: center;
195
+ gap: 1px;
196
+ min-width: 0;
197
+ height: 48px;
198
+ padding: 0;
199
+ border: none;
200
+ border-radius: 0;
201
+ border-bottom: 2px solid transparent;
202
+ background: none;
203
+ color: var(--md-sys-color-on-surface-variant);
204
+ }
205
+ button[tab]:hover:not([on]) {
206
+ background: var(--md-sys-color-surface-container);
207
+ }
208
+ button[tab][on] {
209
+ color: var(--md-sys-color-primary);
210
+ border-bottom-color: var(--md-sys-color-primary);
211
+ }
212
+ button[tab] md-icon {
213
+ font-size: 20px;
214
+ width: 20px;
215
+ height: 20px;
216
+ }
217
+ button[tab] span[tab-label] {
218
+ max-width: 100%;
219
+ overflow: hidden;
220
+ font-size: 0.66rem;
221
+ line-height: 1.2;
222
+ white-space: nowrap;
223
+ text-overflow: ellipsis;
224
+ }
225
+ span[badge] {
226
+ position: absolute;
227
+ top: 3px;
228
+ right: 2px;
229
+ padding: 0 5px;
230
+ font-size: 0.62rem;
231
+ font-weight: 700;
232
+ line-height: 1.5;
233
+ color: var(--md-sys-color-on-primary-container);
234
+ background: var(--md-sys-color-primary-container);
235
+ border-radius: 999em;
236
+ pointer-events: none;
237
+ }
238
+ /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */
239
+ div[panel] {
240
+ flex: 1;
241
+ min-height: 0;
242
+ overflow: auto;
243
+ padding: 0;
244
+ }
245
+ div[panel][flush] {
246
+ padding: 0;
247
+ }
248
+ /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */
249
+ [off] {
250
+ display: none !important;
251
+ }
252
+ /* Which part the panel speaks of. */
253
+ div[subject] {
254
+ position: sticky;
255
+ top: 0;
256
+ z-index: 1;
257
+ display: grid;
258
+ gap: 4px;
259
+ padding: 10px 12px;
260
+ background: var(--md-sys-color-surface-container-lowest);
261
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
262
+ }
263
+ /* The panel's three sizes (side-panel-styles): the part's kind at the title's, its two lines at the hint's. */
264
+ div[subject-summary] {
265
+ display: grid;
266
+ gap: 2px;
267
+ }
268
+ div[subject-summary] b {
269
+ font-size: var(--ui-t-body, 13px);
270
+ font-weight: 600;
271
+ }
272
+ div[subject-summary] b span {
273
+ font-weight: 400;
274
+ color: var(--md-sys-color-on-surface-variant);
275
+ }
276
+ div[subject-summary] small {
277
+ color: var(--md-sys-color-on-surface-variant);
278
+ }
279
+ button {
280
+ font: inherit;
281
+ box-sizing: border-box;
282
+ height: 32px;
283
+ border: 1px solid var(--md-sys-color-outline-variant);
284
+ border-radius: 6px;
285
+ background: var(--md-sys-color-surface);
286
+ color: inherit;
287
+ padding: 0 8px;
288
+ min-width: 0;
289
+ }
290
+ button {
291
+ cursor: pointer;
292
+ }
293
+ button:disabled {
294
+ opacity: 0.45;
295
+ cursor: default;
296
+ }
297
+ button[aria-pressed='true'] {
298
+ background: var(--md-sys-color-secondary-container);
299
+ color: var(--md-sys-color-on-secondary-container);
300
+ border-color: var(--md-sys-color-primary);
301
+ }
302
+ .hint {
303
+ color: var(--md-sys-color-on-surface-variant);
304
+ line-height: 1.5;
305
+ }
306
+ `;
307
+ //# sourceMappingURL=editor-styles.js.map
@@ -0,0 +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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+S9B,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 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 font: 13px sans-serif;\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 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 aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\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: 0.66rem;\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: 0.62rem;\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 /* 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 button {\n font: inherit;\n box-sizing: border-box;\n height: 32px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 6px;\n background: var(--md-sys-color-surface);\n color: inherit;\n padding: 0 8px;\n min-width: 0;\n }\n button {\n cursor: pointer;\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"]}
@@ -77,43 +77,19 @@ export declare class FigureEditor extends LitElement {
77
77
  private live;
78
78
  /** The part whose drawn section is open in the section editor (ADR-0100 §3.2), or null. */
79
79
  private sectionEditing;
80
- /**
81
- * A command seen before it is done (ADR-0097 §5.5): worked out on a copy for the figure, version, part and command it
82
- * names, and shown only while all four are still the ones on screen. Nothing of it is saved or undone.
83
- */
84
- private preview?;
85
- private previewDrawn?;
86
80
  private lastContext;
81
+ private commandPreview;
87
82
  /**
88
83
  * Move, turn and size on the canvas, in the bar's tools: the gizmo the W · E · R keys give, found without knowing the
89
84
  * keys (2026-09-26 -- a person dragging the part turned the camera). Each is offered only where the part's own
90
85
  * numbers can take it (`gizmoReachOf`).
91
86
  */
92
87
  private renderGizmoTools;
93
- /** The modeller's keys (`keys.ts`): undo, redo, duplicate, remove, back to the whole figure, the gizmo's modes. */
94
- private onKey;
95
- connectedCallback(): void;
96
- disconnectedCallback(): void;
97
- private static readonly FOLD_START_UNDER;
98
- private foldedForWidth;
99
- private keptOpenByAuthor;
100
- private widthWatch;
101
- private fitPanesTo;
102
- /**
103
- * Refusals and notices go to the app's shared surfaces, as every screen's do (user, 2026-09-27: 「왜 모두가 사용하는
104
- * notification 오류 표현 방법을 사용하지 않고」 · 「혹은 oxprompt 에서 확인하고 진행하던지」). A refusal with a way on --
105
- * delete and fit the space again, narrow the range -- asks in the app's prompt and goes on when confirmed; any
106
- * other refusal is the app's notification, which stays until it is closed.
107
- */
108
- private toldViewError;
109
- private tell;
110
- private askClipLoss;
111
- private confirmClipLoss;
112
- private ask;
88
+ private keys;
89
+ private keyAction;
90
+ private paneFold;
91
+ private refusals;
113
92
  private panelMemory;
114
- private restorePanelFor?;
115
- private propertiesPanel;
116
- private foldsOf;
117
93
  /** The preview is made when first asked for, and kept (hidden) after that; it is taken down with the editor. */
118
94
  private previewMounted;
119
95
  protected willUpdate(_changed: PropertyValues): void;
@@ -209,8 +185,6 @@ export declare class FigureEditor extends LitElement {
209
185
  private renderRepeatNote;
210
186
  private renamePart;
211
187
  /** Size response, fastening and motion for the selected part, as authoring commands. */
212
- private previewWordsSaid?;
213
- private previewWordsNow;
214
188
  private renderLinks;
215
189
  /**
216
190
  * The face a size drag keeps for the selected part, figure-model's rule (`figureScaleAnchorOf`): the gizmo shows it
@@ -273,25 +247,6 @@ export declare class FigureEditor extends LitElement {
273
247
  private releaseWords;
274
248
  /** To the release that exists: the history tab's release section, not a second one here. */
275
249
  private goRelease;
276
- /** The fastening form's command, seen before it is done; null clears what was shown. */
277
- /**
278
- * What a command would do is worked out on a copy of the figure -- several compiles of it -- so it waits for the field to
279
- * rest: a number typed digit by digit asks once, for the number it ends on. Clearing is at once, and the same question on
280
- * the same version is not asked again.
281
- */
282
- private previewTimer?;
283
- private previewCommand;
284
- private computePreview;
285
- /** The preview, while it still speaks of what is on screen: this figure, this version, this part. */
286
- private currentPreview;
287
- private detachTimer?;
288
- private detachAsked?;
289
- /**
290
- * Said once the figure has rested: working it out is a preview of the command on a copy (tens of milliseconds), and the
291
- * edit that made this version should be on screen before it. Until then nothing is said, as for a part with no joint.
292
- */
293
- private detachWords;
294
- private computeDetachWords;
295
250
  private overhangOf;
296
251
  /** The selected part's motion at both ends of its range, made once per version and part. */
297
252
  private motionEnds;