@things-factory/figure-ui 10.1.70 → 10.1.72
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/graphql/index.ts +28 -69
- package/client/modeller/figure-v3-parts.ts +16 -136
- package/client/modeller/figure-v3-recipe-catalog.ts +366 -0
- package/client/modeller/v3-editor.ts +25 -5
- package/client/modeller/v3-fields.ts +21 -16
- package/client/modeller/v3-gizmo.ts +1 -1
- package/client/modeller/v3-links.ts +7 -66
- package/client/modeller/v3-palette.ts +3 -0
- package/client/modeller/v3-recipe-source.ts +15 -0
- package/client/modeller/v3-recipes.ts +50 -27
- package/client/pages/figure-v3-modeller-page.ts +16 -13
- package/client/route.ts +0 -4
- package/dist-client/graphql/index.d.ts +11 -18
- package/dist-client/graphql/index.js +18 -59
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +5 -12
- package/dist-client/modeller/figure-v3-parts.js +17 -147
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-recipe-catalog.d.ts +38 -0
- package/dist-client/modeller/figure-v3-recipe-catalog.js +399 -0
- package/dist-client/modeller/figure-v3-recipe-catalog.js.map +1 -0
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +8 -3
- package/dist-client/modeller/v3-editor.js +35 -6
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +2 -2
- package/dist-client/modeller/v3-fields.js +20 -18
- package/dist-client/modeller/v3-fields.js.map +1 -1
- package/dist-client/modeller/v3-gizmo.js +1 -1
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +8 -25
- package/dist-client/modeller/v3-links.js +4 -66
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-palette.js +3 -0
- package/dist-client/modeller/v3-palette.js.map +1 -1
- package/dist-client/modeller/v3-recipe-source.d.ts +8 -0
- package/dist-client/modeller/v3-recipe-source.js +14 -0
- package/dist-client/modeller/v3-recipe-source.js.map +1 -0
- package/dist-client/modeller/v3-recipes.d.ts +25 -14
- package/dist-client/modeller/v3-recipes.js +42 -28
- package/dist-client/modeller/v3-recipes.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +5 -3
- package/dist-client/pages/figure-v3-modeller-page.js +16 -13
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +0 -3
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/scripts/template-thumbnails.mjs +53 -0
- package/test/browser/build.mjs +15 -3
- package/test/browser/modeller-page.js +26 -3
- package/test/browser/modeller.check.mjs +73 -46
- package/test/browser/serve.mjs +7 -1
- package/test/browser/thumbnail-page.js +14 -0
- package/test/v3-fields.test.ts +5 -1
- package/test/v3-links.test.ts +12 -0
- package/test/v3-recipes.test.ts +47 -25
- package/things-factory.config.js +0 -1
- package/translations/en.json +38 -23
- package/translations/ja.json +38 -23
- package/translations/ko.json +38 -23
- package/translations/ms.json +38 -23
- package/translations/zh.json +38 -23
- package/client/pages/figure-recipe-page.ts +0 -338
- package/dist-client/pages/figure-recipe-page.d.ts +0 -41
- package/dist-client/pages/figure-recipe-page.js +0 -352
- package/dist-client/pages/figure-recipe-page.js.map +0 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import '@operato/markdown';
|
|
4
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
5
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
6
|
+
import { i18next, localize } from '@operato/i18n';
|
|
7
|
+
import { ScrollbarStyles } from '@operato/styles';
|
|
8
|
+
import { describeV3Recipe } from '@hatiolab/figure-model';
|
|
9
|
+
import { recipeFieldOf, recipeReady, recipeValueOf, recipeWord, recipeWorkings, recipesMatching } from './v3-recipes.js';
|
|
10
|
+
/**
|
|
11
|
+
* The recipe catalog (ADR-0095): the templates the product ships, by category, found by words, each told in full --
|
|
12
|
+
* what it puts in, how many parts, how they move, what each value does, and how to use it -- and put onto the chosen
|
|
13
|
+
* part in one step. The numbers and the ways of moving are worked out from the recipe (`describeV3Recipe`), so they say
|
|
14
|
+
* what it does; only the descriptions and the guide are written.
|
|
15
|
+
*/
|
|
16
|
+
let FigureV3RecipeCatalog = class FigureV3RecipeCatalog extends localize(i18next)(LitElement) {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
this.templates = [];
|
|
20
|
+
/** The part it would be put onto. */
|
|
21
|
+
this.selected = '';
|
|
22
|
+
this.locked = false;
|
|
23
|
+
/** Why the last one put in was refused, in the kernel's words. */
|
|
24
|
+
this.error = '';
|
|
25
|
+
this.category = '';
|
|
26
|
+
this.query = '';
|
|
27
|
+
this.chosen = '';
|
|
28
|
+
this.values = {};
|
|
29
|
+
/*
|
|
30
|
+
The guide as a page \`ox-markdown\` can read: it renders what its src points to, not text, and given the text it
|
|
31
|
+
fetched it as a path and drew the app's own page in its place (seen on :3300, 2026-09-26). A blob of type
|
|
32
|
+
text/markdown is read as markdown.
|
|
33
|
+
*/
|
|
34
|
+
this.guide = '';
|
|
35
|
+
}
|
|
36
|
+
static { this.styles = [
|
|
37
|
+
ScrollbarStyles,
|
|
38
|
+
css `
|
|
39
|
+
:host {
|
|
40
|
+
position: fixed;
|
|
41
|
+
inset: 0;
|
|
42
|
+
z-index: 20;
|
|
43
|
+
display: grid;
|
|
44
|
+
place-items: center;
|
|
45
|
+
background: color-mix(in srgb, var(--md-sys-color-scrim, #000) 32%, transparent);
|
|
46
|
+
font-size: 0.82rem;
|
|
47
|
+
color: var(--md-sys-color-on-surface);
|
|
48
|
+
}
|
|
49
|
+
div[panel] {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-rows: auto 1fr;
|
|
52
|
+
width: min(1100px, calc(100vw - 32px));
|
|
53
|
+
height: min(720px, calc(100vh - 32px));
|
|
54
|
+
background: var(--md-sys-color-surface);
|
|
55
|
+
border-radius: 12px;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);
|
|
58
|
+
}
|
|
59
|
+
header {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 8px;
|
|
63
|
+
padding: 8px 12px;
|
|
64
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
65
|
+
}
|
|
66
|
+
header h2 {
|
|
67
|
+
margin: 0;
|
|
68
|
+
font-size: var(--ui-t-title, 16px);
|
|
69
|
+
flex: 1;
|
|
70
|
+
}
|
|
71
|
+
header input {
|
|
72
|
+
width: 220px;
|
|
73
|
+
padding: 4px 8px;
|
|
74
|
+
font: inherit;
|
|
75
|
+
}
|
|
76
|
+
button[close] {
|
|
77
|
+
display: inline-grid;
|
|
78
|
+
place-items: center;
|
|
79
|
+
width: 32px;
|
|
80
|
+
height: 32px;
|
|
81
|
+
border: none;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
background: none;
|
|
84
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
}
|
|
87
|
+
div[body] {
|
|
88
|
+
display: grid;
|
|
89
|
+
grid-template-columns: 170px minmax(0, 1fr) minmax(300px, 380px);
|
|
90
|
+
min-height: 0;
|
|
91
|
+
}
|
|
92
|
+
@media (max-width: 760px) {
|
|
93
|
+
div[body] {
|
|
94
|
+
grid-template-columns: 1fr;
|
|
95
|
+
overflow: auto;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
nav,
|
|
99
|
+
section[list],
|
|
100
|
+
section[detail] {
|
|
101
|
+
overflow: auto;
|
|
102
|
+
min-height: 0;
|
|
103
|
+
padding: 8px;
|
|
104
|
+
}
|
|
105
|
+
nav {
|
|
106
|
+
border-right: 1px solid var(--md-sys-color-outline-variant);
|
|
107
|
+
}
|
|
108
|
+
section[detail] {
|
|
109
|
+
border-left: 1px solid var(--md-sys-color-outline-variant);
|
|
110
|
+
display: grid;
|
|
111
|
+
gap: 10px;
|
|
112
|
+
align-content: start;
|
|
113
|
+
}
|
|
114
|
+
button[category] {
|
|
115
|
+
display: flex;
|
|
116
|
+
justify-content: space-between;
|
|
117
|
+
width: 100%;
|
|
118
|
+
padding: 6px 8px;
|
|
119
|
+
font: inherit;
|
|
120
|
+
color: inherit;
|
|
121
|
+
background: none;
|
|
122
|
+
border: none;
|
|
123
|
+
border-radius: 6px;
|
|
124
|
+
text-align: left;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
}
|
|
127
|
+
button[category][on],
|
|
128
|
+
button[card][on] {
|
|
129
|
+
background: var(--md-sys-color-secondary-container);
|
|
130
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
131
|
+
}
|
|
132
|
+
section[list] {
|
|
133
|
+
display: grid;
|
|
134
|
+
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
|
|
135
|
+
gap: 8px;
|
|
136
|
+
align-content: start;
|
|
137
|
+
}
|
|
138
|
+
button[card] {
|
|
139
|
+
display: grid;
|
|
140
|
+
gap: 4px;
|
|
141
|
+
padding: 8px;
|
|
142
|
+
font: inherit;
|
|
143
|
+
color: inherit;
|
|
144
|
+
text-align: left;
|
|
145
|
+
background: none;
|
|
146
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
147
|
+
border-radius: 8px;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
button[card] img {
|
|
151
|
+
width: 100%;
|
|
152
|
+
aspect-ratio: 4 / 3;
|
|
153
|
+
object-fit: contain;
|
|
154
|
+
background: var(--md-sys-color-surface-container);
|
|
155
|
+
border-radius: 4px;
|
|
156
|
+
}
|
|
157
|
+
button[card] small,
|
|
158
|
+
p[muted] {
|
|
159
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
160
|
+
}
|
|
161
|
+
h3 {
|
|
162
|
+
margin: 0;
|
|
163
|
+
font-size: var(--ui-t-body, 14px);
|
|
164
|
+
}
|
|
165
|
+
p {
|
|
166
|
+
margin: 0;
|
|
167
|
+
line-height: 1.5;
|
|
168
|
+
}
|
|
169
|
+
ul {
|
|
170
|
+
margin: 0;
|
|
171
|
+
padding-left: 18px;
|
|
172
|
+
line-height: 1.6;
|
|
173
|
+
}
|
|
174
|
+
table {
|
|
175
|
+
border-collapse: collapse;
|
|
176
|
+
width: 100%;
|
|
177
|
+
}
|
|
178
|
+
th,
|
|
179
|
+
td {
|
|
180
|
+
text-align: left;
|
|
181
|
+
vertical-align: top;
|
|
182
|
+
padding: 3px 4px;
|
|
183
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
184
|
+
}
|
|
185
|
+
td small {
|
|
186
|
+
display: block;
|
|
187
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
188
|
+
}
|
|
189
|
+
div[form] {
|
|
190
|
+
display: grid;
|
|
191
|
+
gap: 6px;
|
|
192
|
+
padding: 8px;
|
|
193
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
194
|
+
border-radius: 8px;
|
|
195
|
+
}
|
|
196
|
+
div[form] label {
|
|
197
|
+
display: grid;
|
|
198
|
+
gap: 2px;
|
|
199
|
+
}
|
|
200
|
+
div[form] label[toggle] {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: 6px;
|
|
204
|
+
}
|
|
205
|
+
div[form] input,
|
|
206
|
+
div[form] select {
|
|
207
|
+
font: inherit;
|
|
208
|
+
min-width: 0;
|
|
209
|
+
}
|
|
210
|
+
button[insert] {
|
|
211
|
+
justify-self: start;
|
|
212
|
+
display: inline-flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
gap: 4px;
|
|
215
|
+
padding: 6px 14px;
|
|
216
|
+
font: inherit;
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
color: var(--md-sys-color-on-primary);
|
|
219
|
+
background: var(--md-sys-color-primary);
|
|
220
|
+
border: none;
|
|
221
|
+
border-radius: 999em;
|
|
222
|
+
cursor: pointer;
|
|
223
|
+
}
|
|
224
|
+
button[insert]:disabled {
|
|
225
|
+
opacity: 0.38;
|
|
226
|
+
cursor: default;
|
|
227
|
+
}
|
|
228
|
+
p[role='alert'] {
|
|
229
|
+
padding: 6px;
|
|
230
|
+
white-space: pre-wrap;
|
|
231
|
+
color: var(--md-sys-color-on-error-container);
|
|
232
|
+
background: var(--md-sys-color-error-container);
|
|
233
|
+
border-radius: 6px;
|
|
234
|
+
}
|
|
235
|
+
ox-markdown {
|
|
236
|
+
display: block;
|
|
237
|
+
}
|
|
238
|
+
`
|
|
239
|
+
]; }
|
|
240
|
+
set guideText(text) {
|
|
241
|
+
if (this.guide)
|
|
242
|
+
URL.revokeObjectURL(this.guide);
|
|
243
|
+
this.guide = text ? URL.createObjectURL(new Blob([text], { type: 'text/markdown' })) : '';
|
|
244
|
+
}
|
|
245
|
+
disconnectedCallback() {
|
|
246
|
+
super.disconnectedCallback();
|
|
247
|
+
this.guideText = '';
|
|
248
|
+
}
|
|
249
|
+
render() {
|
|
250
|
+
const language = i18next.language || 'en';
|
|
251
|
+
const categories = [...new Set(this.templates.map(t => t.category))].sort();
|
|
252
|
+
const shown = recipesMatching(this.templates, this.category, this.query);
|
|
253
|
+
const chosen = this.templates.find(t => t.name === this.chosen) ?? shown[0];
|
|
254
|
+
return html `<div panel role="dialog" aria-modal="true" aria-label=${i18next.t('figure.label.recipe-catalog')}>
|
|
255
|
+
<header>
|
|
256
|
+
<h2>${i18next.t('figure.label.recipe-catalog')}</h2>
|
|
257
|
+
<input search type="search" placeholder=${i18next.t('figure.text.recipe-search')} .value=${this.query} @input=${(e) => (this.query = e.target.value)} />
|
|
258
|
+
<button close aria-label=${i18next.t('figure.button.close')} @click=${() => this.dispatchEvent(new CustomEvent('close-catalog', { bubbles: true, composed: true }))}>
|
|
259
|
+
<md-icon>close</md-icon>
|
|
260
|
+
</button>
|
|
261
|
+
</header>
|
|
262
|
+
<div body>
|
|
263
|
+
<nav>
|
|
264
|
+
<button category ?on=${!this.category} @click=${() => (this.category = '')}>
|
|
265
|
+
<span>${i18next.t('figure.label.recipe-all')}</span><small>${this.templates.length}</small>
|
|
266
|
+
</button>
|
|
267
|
+
${categories.map(c => html `<button category=${c} ?on=${this.category === c} @click=${() => (this.category = c)}>
|
|
268
|
+
<span>${i18next.t('figure.label.recipe-category-' + c)}</span><small>${this.templates.filter(t => t.category === c).length}</small>
|
|
269
|
+
</button>`)}
|
|
270
|
+
</nav>
|
|
271
|
+
<section list>
|
|
272
|
+
${shown.map(t => html `<button card=${t.name} ?on=${t.name === chosen?.name} @click=${() => this.choose(t)}>
|
|
273
|
+
${t.hasThumbnail && this.thumbnailOf ? html `<img alt="" loading="lazy" src=${this.thumbnailOf(t.name)} />` : nothing}
|
|
274
|
+
<b>${t.title}</b>
|
|
275
|
+
<small>${t.description}</small>
|
|
276
|
+
</button>`)}
|
|
277
|
+
</section>
|
|
278
|
+
<section detail>${chosen ? this.renderDetail(chosen, language) : nothing}</section>
|
|
279
|
+
</div>
|
|
280
|
+
</div>`;
|
|
281
|
+
}
|
|
282
|
+
choose(t) {
|
|
283
|
+
if (this.chosen === t.name)
|
|
284
|
+
return;
|
|
285
|
+
this.chosen = t.name;
|
|
286
|
+
this.values = Object.fromEntries(t.recipe.params.map(p => [p.name, p.default]));
|
|
287
|
+
this.guideText = '';
|
|
288
|
+
this.guideOf?.(t.name)
|
|
289
|
+
.then(text => this.chosen === t.name && (this.guideText = text))
|
|
290
|
+
.catch(() => { });
|
|
291
|
+
}
|
|
292
|
+
willUpdate() {
|
|
293
|
+
/*
|
|
294
|
+
The first card shown is chosen while none of those shown is, so the detail is never empty and never of a card the
|
|
295
|
+
search or the category has taken away (the air cylinder stayed told under a search for rollers, :3300, 2026-09-26).
|
|
296
|
+
*/
|
|
297
|
+
const shown = recipesMatching(this.templates, this.category, this.query);
|
|
298
|
+
if (!shown.some(t => t.name === this.chosen) && shown[0])
|
|
299
|
+
this.choose(shown[0]);
|
|
300
|
+
}
|
|
301
|
+
renderDetail(t, language) {
|
|
302
|
+
const d = describeV3Recipe(t.recipe);
|
|
303
|
+
const count = d.parts.min === d.parts.max ? String(d.parts.min) : `${d.parts.min}–${d.parts.max}`;
|
|
304
|
+
const ready = recipeReady(t.recipe, this.selected);
|
|
305
|
+
return html `
|
|
306
|
+
<h3>${t.title}</h3>
|
|
307
|
+
<p>${t.description}</p>
|
|
308
|
+
<div>
|
|
309
|
+
<h3>${i18next.t('figure.label.recipe-adds')}</h3>
|
|
310
|
+
<ul>
|
|
311
|
+
<li parts=${count}>${i18next.t("figure.text.recipe-parts", { parts: count })}</li>
|
|
312
|
+
${recipeWorkings(t.recipe).map(line => html `<li>${i18next.t(line.key, line.words)}</li>`)}
|
|
313
|
+
<li>${i18next.t(t.recipe.target === 'part' ? 'figure.text.recipe-onto-chosen' : 'figure.text.recipe-onto-figure')}</li>
|
|
314
|
+
</ul>
|
|
315
|
+
</div>
|
|
316
|
+
${t.recipe.params.length ? this.renderForm(t.recipe, language) : nothing}
|
|
317
|
+
${this.error ? html `<p role="alert">${this.error}</p>` : nothing}
|
|
318
|
+
${ready ? nothing : html `<p muted>${i18next.t('figure.text.recipe-needs-a-part')}</p>`}
|
|
319
|
+
<button insert ?disabled=${this.locked || !ready} @click=${() => this.dispatchEvent(new CustomEvent('insert-recipe', { detail: { recipe: t.recipe, values: this.values }, bubbles: true, composed: true }))}>
|
|
320
|
+
<md-icon>add</md-icon>${this.selected && t.recipe.target === 'part' ? i18next.t('figure.button.insert-recipe-onto', { part: this.selected }) : i18next.t('figure.button.insert-recipe')}
|
|
321
|
+
</button>
|
|
322
|
+
${this.guide ? html `<ox-markdown .src=${this.guide}></ox-markdown>` : nothing}
|
|
323
|
+
`;
|
|
324
|
+
}
|
|
325
|
+
/** Each value: its name, what it does, and the field to set it -- a share typed as a percentage. */
|
|
326
|
+
renderForm(recipe, language) {
|
|
327
|
+
const set = (name, value) => (this.values = { ...this.values, [name]: value });
|
|
328
|
+
return html `<div form>
|
|
329
|
+
<h3>${i18next.t('figure.label.recipe-values')}</h3>
|
|
330
|
+
${recipe.params.map(param => {
|
|
331
|
+
const label = recipeWord(param.label, language) || param.name;
|
|
332
|
+
const hint = recipeWord(param.hint, language);
|
|
333
|
+
const value = this.values[param.name] ?? param.default;
|
|
334
|
+
const help = hint ? html `<small>${hint}</small>` : nothing;
|
|
335
|
+
if (param.kind === 'flag')
|
|
336
|
+
return html `<label toggle><input type="checkbox" param=${param.name} .checked=${value === true} @change=${(e) => set(param.name, e.target.checked)} />${label}</label>${help}`;
|
|
337
|
+
if (param.kind === 'choice' || param.kind === 'face')
|
|
338
|
+
return html `<label><span>${label}</span>
|
|
339
|
+
<select param=${param.name} @change=${(e) => set(param.name, recipeValueOf(param, e.target.value))}>
|
|
340
|
+
${(param.options ?? []).map(o => html `<option value=${String(o)} ?selected=${o === value}>${String(o)}</option>`)}
|
|
341
|
+
</select></label
|
|
342
|
+
>${help}`;
|
|
343
|
+
const range = param.kind === 'ratio' ? `${recipeFieldOf(param, param.min ?? 0)}–${recipeFieldOf(param, param.max ?? 1)} %` : `${param.min ?? ''}–${param.max ?? ''} mm`;
|
|
344
|
+
return html `<label><span>${label} (${range})</span>
|
|
345
|
+
<input type="number" step="any" param=${param.name} .value=${recipeFieldOf(param, value)} @change=${(e) => set(param.name, recipeValueOf(param, e.target.value))} /></label
|
|
346
|
+
>${help}`;
|
|
347
|
+
})}
|
|
348
|
+
</div>`;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
__decorate([
|
|
352
|
+
property({ attribute: false }),
|
|
353
|
+
__metadata("design:type", Array)
|
|
354
|
+
], FigureV3RecipeCatalog.prototype, "templates", void 0);
|
|
355
|
+
__decorate([
|
|
356
|
+
property({ type: String }),
|
|
357
|
+
__metadata("design:type", Object)
|
|
358
|
+
], FigureV3RecipeCatalog.prototype, "selected", void 0);
|
|
359
|
+
__decorate([
|
|
360
|
+
property({ type: Boolean }),
|
|
361
|
+
__metadata("design:type", Object)
|
|
362
|
+
], FigureV3RecipeCatalog.prototype, "locked", void 0);
|
|
363
|
+
__decorate([
|
|
364
|
+
property({ type: String }),
|
|
365
|
+
__metadata("design:type", Object)
|
|
366
|
+
], FigureV3RecipeCatalog.prototype, "error", void 0);
|
|
367
|
+
__decorate([
|
|
368
|
+
property({ attribute: false }),
|
|
369
|
+
__metadata("design:type", Function)
|
|
370
|
+
], FigureV3RecipeCatalog.prototype, "guideOf", void 0);
|
|
371
|
+
__decorate([
|
|
372
|
+
property({ attribute: false }),
|
|
373
|
+
__metadata("design:type", Function)
|
|
374
|
+
], FigureV3RecipeCatalog.prototype, "thumbnailOf", void 0);
|
|
375
|
+
__decorate([
|
|
376
|
+
state(),
|
|
377
|
+
__metadata("design:type", Object)
|
|
378
|
+
], FigureV3RecipeCatalog.prototype, "category", void 0);
|
|
379
|
+
__decorate([
|
|
380
|
+
state(),
|
|
381
|
+
__metadata("design:type", Object)
|
|
382
|
+
], FigureV3RecipeCatalog.prototype, "query", void 0);
|
|
383
|
+
__decorate([
|
|
384
|
+
state(),
|
|
385
|
+
__metadata("design:type", Object)
|
|
386
|
+
], FigureV3RecipeCatalog.prototype, "chosen", void 0);
|
|
387
|
+
__decorate([
|
|
388
|
+
state(),
|
|
389
|
+
__metadata("design:type", Object)
|
|
390
|
+
], FigureV3RecipeCatalog.prototype, "values", void 0);
|
|
391
|
+
__decorate([
|
|
392
|
+
state(),
|
|
393
|
+
__metadata("design:type", Object)
|
|
394
|
+
], FigureV3RecipeCatalog.prototype, "guide", void 0);
|
|
395
|
+
FigureV3RecipeCatalog = __decorate([
|
|
396
|
+
customElement('figure-v3-recipe-catalog')
|
|
397
|
+
], FigureV3RecipeCatalog);
|
|
398
|
+
export { FigureV3RecipeCatalog };
|
|
399
|
+
//# sourceMappingURL=figure-v3-recipe-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-v3-recipe-catalog.js","sourceRoot":"","sources":["../../client/modeller/figure-v3-recipe-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '@operato/markdown'\n\nimport { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { ScrollbarStyles } from '@operato/styles'\nimport { describeV3Recipe } from '@hatiolab/figure-model'\nimport type { V3Recipe, V3RecipeValue } from '@hatiolab/figure-model'\n\nimport { recipeFieldOf, recipeReady, recipeValueOf, recipeWord, recipeWorkings, recipesMatching, type RecipeTemplate } from './v3-recipes.js'\n\n/**\n * The recipe catalog (ADR-0095): the templates the product ships, by category, found by words, each told in full --\n * what it puts in, how many parts, how they move, what each value does, and how to use it -- and put onto the chosen\n * part in one step. The numbers and the ways of moving are worked out from the recipe (`describeV3Recipe`), so they say\n * what it does; only the descriptions and the guide are written.\n */\n@customElement('figure-v3-recipe-catalog')\nexport class FigureV3RecipeCatalog extends localize(i18next)(LitElement) {\n static styles = [\n ScrollbarStyles,\n css`\n :host {\n position: fixed;\n inset: 0;\n z-index: 20;\n display: grid;\n place-items: center;\n background: color-mix(in srgb, var(--md-sys-color-scrim, #000) 32%, transparent);\n font-size: 0.82rem;\n color: var(--md-sys-color-on-surface);\n }\n div[panel] {\n display: grid;\n grid-template-rows: auto 1fr;\n width: min(1100px, calc(100vw - 32px));\n height: min(720px, calc(100vh - 32px));\n background: var(--md-sys-color-surface);\n border-radius: 12px;\n overflow: hidden;\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.28);\n }\n header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n header h2 {\n margin: 0;\n font-size: var(--ui-t-title, 16px);\n flex: 1;\n }\n header input {\n width: 220px;\n padding: 4px 8px;\n font: inherit;\n }\n button[close] {\n display: inline-grid;\n place-items: center;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 8px;\n background: none;\n color: var(--md-sys-color-on-surface-variant);\n cursor: pointer;\n }\n div[body] {\n display: grid;\n grid-template-columns: 170px minmax(0, 1fr) minmax(300px, 380px);\n min-height: 0;\n }\n @media (max-width: 760px) {\n div[body] {\n grid-template-columns: 1fr;\n overflow: auto;\n }\n }\n nav,\n section[list],\n section[detail] {\n overflow: auto;\n min-height: 0;\n padding: 8px;\n }\n nav {\n border-right: 1px solid var(--md-sys-color-outline-variant);\n }\n section[detail] {\n border-left: 1px solid var(--md-sys-color-outline-variant);\n display: grid;\n gap: 10px;\n align-content: start;\n }\n button[category] {\n display: flex;\n justify-content: space-between;\n width: 100%;\n padding: 6px 8px;\n font: inherit;\n color: inherit;\n background: none;\n border: none;\n border-radius: 6px;\n text-align: left;\n cursor: pointer;\n }\n button[category][on],\n button[card][on] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n section[list] {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));\n gap: 8px;\n align-content: start;\n }\n button[card] {\n display: grid;\n gap: 4px;\n padding: 8px;\n font: inherit;\n color: inherit;\n text-align: left;\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n cursor: pointer;\n }\n button[card] img {\n width: 100%;\n aspect-ratio: 4 / 3;\n object-fit: contain;\n background: var(--md-sys-color-surface-container);\n border-radius: 4px;\n }\n button[card] small,\n p[muted] {\n color: var(--md-sys-color-on-surface-variant);\n }\n h3 {\n margin: 0;\n font-size: var(--ui-t-body, 14px);\n }\n p {\n margin: 0;\n line-height: 1.5;\n }\n ul {\n margin: 0;\n padding-left: 18px;\n line-height: 1.6;\n }\n table {\n border-collapse: collapse;\n width: 100%;\n }\n th,\n td {\n text-align: left;\n vertical-align: top;\n padding: 3px 4px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n td small {\n display: block;\n color: var(--md-sys-color-on-surface-variant);\n }\n div[form] {\n display: grid;\n gap: 6px;\n padding: 8px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n }\n div[form] label {\n display: grid;\n gap: 2px;\n }\n div[form] label[toggle] {\n display: flex;\n align-items: center;\n gap: 6px;\n }\n div[form] input,\n div[form] select {\n font: inherit;\n min-width: 0;\n }\n button[insert] {\n justify-self: start;\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 6px 14px;\n font: inherit;\n white-space: nowrap;\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border: none;\n border-radius: 999em;\n cursor: pointer;\n }\n button[insert]:disabled {\n opacity: 0.38;\n cursor: default;\n }\n p[role='alert'] {\n padding: 6px;\n white-space: pre-wrap;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n border-radius: 6px;\n }\n ox-markdown {\n display: block;\n }\n `\n ]\n\n @property({ attribute: false }) templates: RecipeTemplate[] = []\n /** The part it would be put onto. */\n @property({ type: String }) selected = ''\n @property({ type: Boolean }) locked = false\n /** Why the last one put in was refused, in the kernel's words. */\n @property({ type: String }) error = ''\n /** Reads a template's guide; absent, no guide is shown. */\n @property({ attribute: false }) guideOf?: (name: string) => Promise<string>\n /** Where a template's thumbnail is read from; absent, cards show no picture. */\n @property({ attribute: false }) thumbnailOf?: (name: string) => string\n\n @state() private category = ''\n @state() private query = ''\n @state() private chosen = ''\n @state() private values: Record<string, V3RecipeValue> = {}\n /*\n The guide as a page \\`ox-markdown\\` can read: it renders what its src points to, not text, and given the text it\n fetched it as a path and drew the app's own page in its place (seen on :3300, 2026-09-26). A blob of type\n text/markdown is read as markdown.\n */\n @state() private guide = ''\n\n private set guideText(text: string) {\n if (this.guide) URL.revokeObjectURL(this.guide)\n this.guide = text ? URL.createObjectURL(new Blob([text], { type: 'text/markdown' })) : ''\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.guideText = ''\n }\n\n render() {\n const language = i18next.language || 'en'\n const categories = [...new Set(this.templates.map(t => t.category))].sort()\n const shown = recipesMatching(this.templates, this.category, this.query)\n const chosen = this.templates.find(t => t.name === this.chosen) ?? shown[0]\n return html`<div panel role=\"dialog\" aria-modal=\"true\" aria-label=${i18next.t('figure.label.recipe-catalog')}>\n <header>\n <h2>${i18next.t('figure.label.recipe-catalog')}</h2>\n <input search type=\"search\" placeholder=${i18next.t('figure.text.recipe-search')} .value=${this.query} @input=${(e: Event) => (this.query = (e.target as HTMLInputElement).value)} />\n <button close aria-label=${i18next.t('figure.button.close')} @click=${() => this.dispatchEvent(new CustomEvent('close-catalog', { bubbles: true, composed: true }))}>\n <md-icon>close</md-icon>\n </button>\n </header>\n <div body>\n <nav>\n <button category ?on=${!this.category} @click=${() => (this.category = '')}>\n <span>${i18next.t('figure.label.recipe-all')}</span><small>${this.templates.length}</small>\n </button>\n ${categories.map(\n c => html`<button category=${c} ?on=${this.category === c} @click=${() => (this.category = c)}>\n <span>${i18next.t('figure.label.recipe-category-' + c)}</span><small>${this.templates.filter(t => t.category === c).length}</small>\n </button>`\n )}\n </nav>\n <section list>\n ${shown.map(\n t => html`<button card=${t.name} ?on=${t.name === chosen?.name} @click=${() => this.choose(t)}>\n ${t.hasThumbnail && this.thumbnailOf ? html`<img alt=\"\" loading=\"lazy\" src=${this.thumbnailOf(t.name)} />` : nothing}\n <b>${t.title}</b>\n <small>${t.description}</small>\n </button>`\n )}\n </section>\n <section detail>${chosen ? this.renderDetail(chosen, language) : nothing}</section>\n </div>\n </div>`\n }\n\n private choose(t: RecipeTemplate) {\n if (this.chosen === t.name) return\n this.chosen = t.name\n this.values = Object.fromEntries(t.recipe.params.map(p => [p.name, p.default]))\n this.guideText = ''\n this.guideOf?.(t.name)\n .then(text => this.chosen === t.name && (this.guideText = text))\n .catch(() => {})\n }\n\n protected willUpdate() {\n /*\n The first card shown is chosen while none of those shown is, so the detail is never empty and never of a card the\n search or the category has taken away (the air cylinder stayed told under a search for rollers, :3300, 2026-09-26).\n */\n const shown = recipesMatching(this.templates, this.category, this.query)\n if (!shown.some(t => t.name === this.chosen) && shown[0]) this.choose(shown[0])\n }\n\n private renderDetail(t: RecipeTemplate, language: string) {\n const d = describeV3Recipe(t.recipe)\n const count = d.parts.min === d.parts.max ? String(d.parts.min) : `${d.parts.min}–${d.parts.max}`\n const ready = recipeReady(t.recipe, this.selected)\n return html`\n <h3>${t.title}</h3>\n <p>${t.description}</p>\n <div>\n <h3>${i18next.t('figure.label.recipe-adds')}</h3>\n <ul>\n <li parts=${count}>${i18next.t(\"figure.text.recipe-parts\", { parts: count })}</li>\n ${recipeWorkings(t.recipe).map(line => html`<li>${i18next.t(line.key, line.words)}</li>`)}\n <li>${i18next.t(t.recipe.target === 'part' ? 'figure.text.recipe-onto-chosen' : 'figure.text.recipe-onto-figure')}</li>\n </ul>\n </div>\n ${t.recipe.params.length ? this.renderForm(t.recipe, language) : nothing}\n ${this.error ? html`<p role=\"alert\">${this.error}</p>` : nothing}\n ${ready ? nothing : html`<p muted>${i18next.t('figure.text.recipe-needs-a-part')}</p>`}\n <button insert ?disabled=${this.locked || !ready} @click=${() => this.dispatchEvent(new CustomEvent('insert-recipe', { detail: { recipe: t.recipe, values: this.values }, bubbles: true, composed: true }))}>\n <md-icon>add</md-icon>${this.selected && t.recipe.target === 'part' ? i18next.t('figure.button.insert-recipe-onto', { part: this.selected }) : i18next.t('figure.button.insert-recipe')}\n </button>\n ${this.guide ? html`<ox-markdown .src=${this.guide}></ox-markdown>` : nothing}\n `\n }\n\n /** Each value: its name, what it does, and the field to set it -- a share typed as a percentage. */\n private renderForm(recipe: V3Recipe, language: string) {\n const set = (name: string, value: V3RecipeValue) => (this.values = { ...this.values, [name]: value })\n return html`<div form>\n <h3>${i18next.t('figure.label.recipe-values')}</h3>\n ${recipe.params.map(param => {\n const label = recipeWord(param.label, language) || param.name\n const hint = recipeWord(param.hint, language)\n const value = this.values[param.name] ?? param.default\n const help = hint ? html`<small>${hint}</small>` : nothing\n if (param.kind === 'flag')\n return html`<label toggle><input type=\"checkbox\" param=${param.name} .checked=${value === true} @change=${(e: Event) => set(param.name, (e.target as HTMLInputElement).checked)} />${label}</label>${help}`\n if (param.kind === 'choice' || param.kind === 'face')\n return html`<label><span>${label}</span>\n <select param=${param.name} @change=${(e: Event) => set(param.name, recipeValueOf(param, (e.target as HTMLSelectElement).value))}>\n ${(param.options ?? []).map(o => html`<option value=${String(o)} ?selected=${o === value}>${String(o)}</option>`)}\n </select></label\n >${help}`\n const range = param.kind === 'ratio' ? `${recipeFieldOf(param, param.min ?? 0)}–${recipeFieldOf(param, param.max ?? 1)} %` : `${param.min ?? ''}–${param.max ?? ''} mm`\n return html`<label><span>${label} (${range})</span>\n <input type=\"number\" step=\"any\" param=${param.name} .value=${recipeFieldOf(param, value)} @change=${(e: Event) => set(param.name, recipeValueOf(param, (e.target as HTMLInputElement).value))} /></label\n >${help}`\n })}\n </div>`\n }\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LitElement, type PropertyValues } from 'lit';
|
|
2
2
|
import '@operato/layout';
|
|
3
|
-
import type {
|
|
3
|
+
import type { RecipeTemplate } from './v3-recipes.js';
|
|
4
|
+
import './figure-v3-recipe-catalog.js';
|
|
4
5
|
import type { V3ProductSession } from './v3-product-session.js';
|
|
5
6
|
import './v3-canvas.js';
|
|
6
7
|
import './figure-v3-parts.js';
|
|
@@ -17,8 +18,12 @@ import type { FirstSave } from './v3-first-save.js';
|
|
|
17
18
|
export declare class FigureV3Editor extends LitElement {
|
|
18
19
|
static styles: import("lit").CSSResult[];
|
|
19
20
|
session?: V3ProductSession;
|
|
20
|
-
/** The
|
|
21
|
-
|
|
21
|
+
/** The recipe templates (ADR-0095), read by the page from the server. */
|
|
22
|
+
templates: RecipeTemplate[];
|
|
23
|
+
/** Reads a template's guide; given by the page. */
|
|
24
|
+
guideOf?: (name: string) => Promise<string>;
|
|
25
|
+
thumbnailOf?: (name: string) => string;
|
|
26
|
+
private catalogOpen;
|
|
22
27
|
/** Why the last recipe was refused, in the kernel's words. */
|
|
23
28
|
private recipeError;
|
|
24
29
|
revision: number;
|
|
@@ -6,6 +6,7 @@ import '@operato/layout';
|
|
|
6
6
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
7
7
|
import { live } from 'lit/directives/live.js';
|
|
8
8
|
import { PART_LIMIT, parseV3Recipe, resolveV3Recipe, v3PartCountOf, v3RecipeIdFor } from '@hatiolab/figure-model';
|
|
9
|
+
import './figure-v3-recipe-catalog.js';
|
|
9
10
|
import { captureV3Thumbnail } from './v3-thumbnail.js';
|
|
10
11
|
import { drawableV3Parts } from './v3-renderer.js';
|
|
11
12
|
import './v3-canvas.js';
|
|
@@ -35,8 +36,9 @@ import { liveOverSession, NOTHING_LIVE, scrubbed, scrubStep } from './v3-live.js
|
|
|
35
36
|
let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
36
37
|
constructor() {
|
|
37
38
|
super(...arguments);
|
|
38
|
-
/** The
|
|
39
|
-
this.
|
|
39
|
+
/** The recipe templates (ADR-0095), read by the page from the server. */
|
|
40
|
+
this.templates = [];
|
|
41
|
+
this.catalogOpen = false;
|
|
40
42
|
/** Why the last recipe was refused, in the kernel's words. */
|
|
41
43
|
this.recipeError = '';
|
|
42
44
|
this.revision = 0;
|
|
@@ -529,6 +531,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
529
531
|
const steps = resolveV3Recipe(checked, { id, ...(checked.target === 'part' ? { target: this.selected } : {}), values });
|
|
530
532
|
this.session.applyRecipe(steps);
|
|
531
533
|
this.recipeError = '';
|
|
534
|
+
this.catalogOpen = false;
|
|
532
535
|
this.error = '';
|
|
533
536
|
this.changed();
|
|
534
537
|
}
|
|
@@ -1005,6 +1008,9 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1005
1008
|
const repeat = repeatOf(this.selected, geometry);
|
|
1006
1009
|
if (!asset || !repeat || repeat.index)
|
|
1007
1010
|
return nothing;
|
|
1011
|
+
/* The row is set on its part; what hangs from that part is copied with it and has no row of its own. */
|
|
1012
|
+
if (!asset.document.model.nodes.some(n => n.id === `${this.selected}.repeat`))
|
|
1013
|
+
return nothing;
|
|
1008
1014
|
const model = asset.document.model;
|
|
1009
1015
|
const layout = model.nodes.find(n => n.id === `${this.selected}.layout`);
|
|
1010
1016
|
const axis = model.nodes.find(n => n.id === `${this.selected}.repeat`)?.params?.axis ?? '?';
|
|
@@ -1160,6 +1166,18 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1160
1166
|
</div>`
|
|
1161
1167
|
: null}
|
|
1162
1168
|
${this.notice ? html `<div role="status" class="notice">${this.notice}</div>` : null}
|
|
1169
|
+
${this.catalogOpen
|
|
1170
|
+
? html `<figure-v3-recipe-catalog
|
|
1171
|
+
.templates=${this.templates}
|
|
1172
|
+
.selected=${this.selected}
|
|
1173
|
+
.error=${this.recipeError}
|
|
1174
|
+
.guideOf=${this.guideOf}
|
|
1175
|
+
.thumbnailOf=${this.thumbnailOf}
|
|
1176
|
+
?locked=${unavailable}
|
|
1177
|
+
@insert-recipe=${(e) => this.insertRecipe(e.detail.recipe, e.detail.values)}
|
|
1178
|
+
@close-catalog=${() => (this.catalogOpen = false)}
|
|
1179
|
+
></figure-v3-recipe-catalog>`
|
|
1180
|
+
: null}
|
|
1163
1181
|
<ox-three-pane>
|
|
1164
1182
|
<figure-v3-parts
|
|
1165
1183
|
slot="start"
|
|
@@ -1171,9 +1189,8 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
|
|
|
1171
1189
|
.selected=${this.selected}
|
|
1172
1190
|
?locked=${unavailable}
|
|
1173
1191
|
@add-part=${(e) => this.add(e.detail.kind)}
|
|
1174
|
-
.
|
|
1175
|
-
|
|
1176
|
-
@insert-recipe=${(e) => this.insertRecipe(e.detail.recipe, e.detail.values)}
|
|
1192
|
+
.recipeCount=${this.templates.length}
|
|
1193
|
+
@open-recipe-catalog=${() => ((this.catalogOpen = true), (this.recipeError = ''))}
|
|
1177
1194
|
@select-part=${(e) => this.choose(e.detail.id)}
|
|
1178
1195
|
@toggle-part-hidden=${(e) => this.toggleHiddenPart(e.detail.id)}
|
|
1179
1196
|
@set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => ownerOf(g.id))) : new Set())}
|
|
@@ -1327,7 +1344,19 @@ __decorate([
|
|
|
1327
1344
|
__decorate([
|
|
1328
1345
|
property({ attribute: false }),
|
|
1329
1346
|
__metadata("design:type", Array)
|
|
1330
|
-
], FigureV3Editor.prototype, "
|
|
1347
|
+
], FigureV3Editor.prototype, "templates", void 0);
|
|
1348
|
+
__decorate([
|
|
1349
|
+
property({ attribute: false }),
|
|
1350
|
+
__metadata("design:type", Function)
|
|
1351
|
+
], FigureV3Editor.prototype, "guideOf", void 0);
|
|
1352
|
+
__decorate([
|
|
1353
|
+
property({ attribute: false }),
|
|
1354
|
+
__metadata("design:type", Function)
|
|
1355
|
+
], FigureV3Editor.prototype, "thumbnailOf", void 0);
|
|
1356
|
+
__decorate([
|
|
1357
|
+
state(),
|
|
1358
|
+
__metadata("design:type", Object)
|
|
1359
|
+
], FigureV3Editor.prototype, "catalogOpen", void 0);
|
|
1331
1360
|
__decorate([
|
|
1332
1361
|
state(),
|
|
1333
1362
|
__metadata("design:type", Object)
|