@things-factory/figure-ui 10.1.272 → 10.1.273

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/data-interface.ts +30 -18
  2. package/client/modeller/editor-styles.ts +17 -6
  3. package/client/modeller/editor.ts +2 -0
  4. package/client/modeller/fasten-form.ts +12 -2
  5. package/client/modeller/fasten-geometry.ts +5 -3
  6. package/client/modeller/figure-data-panel.ts +384 -200
  7. package/client/modeller/figure-fasten-drawing.ts +21 -13
  8. package/client/modeller/figure-links.ts +24 -9
  9. package/client/modeller/figure-parts.ts +7 -11
  10. package/client/modeller/figure-preview.ts +258 -76
  11. package/dist-client/modeller/clips.d.ts +1 -4
  12. package/dist-client/modeller/data-interface.d.ts +11 -8
  13. package/dist-client/modeller/data-interface.js +27 -17
  14. package/dist-client/modeller/data-interface.js.map +1 -1
  15. package/dist-client/modeller/editor-styles.js +17 -6
  16. package/dist-client/modeller/editor-styles.js.map +1 -1
  17. package/dist-client/modeller/editor.js +2 -0
  18. package/dist-client/modeller/editor.js.map +1 -1
  19. package/dist-client/modeller/fasten-form.d.ts +5 -0
  20. package/dist-client/modeller/fasten-form.js +8 -2
  21. package/dist-client/modeller/fasten-form.js.map +1 -1
  22. package/dist-client/modeller/fasten-geometry.d.ts +3 -2
  23. package/dist-client/modeller/fasten-geometry.js +5 -3
  24. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  25. package/dist-client/modeller/figure-data-panel.d.ts +10 -19
  26. package/dist-client/modeller/figure-data-panel.js +382 -190
  27. package/dist-client/modeller/figure-data-panel.js.map +1 -1
  28. package/dist-client/modeller/figure-fasten-drawing.js +21 -13
  29. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  30. package/dist-client/modeller/figure-links.js +31 -16
  31. package/dist-client/modeller/figure-links.js.map +1 -1
  32. package/dist-client/modeller/figure-parts.js +7 -11
  33. package/dist-client/modeller/figure-parts.js.map +1 -1
  34. package/dist-client/modeller/figure-preview.d.ts +13 -0
  35. package/dist-client/modeller/figure-preview.js +257 -77
  36. package/dist-client/modeller/figure-preview.js.map +1 -1
  37. package/dist-client/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +8 -7
  39. package/test/browser/editing-page.js +1 -1
  40. package/test/browser/links.check.mjs +30 -0
  41. package/test/browser/modeller.check.mjs +2 -2
  42. package/test/browser/parts-page.js +4 -0
  43. package/test/browser/parts.check.mjs +16 -10
  44. package/test/browser/screen-page.js +12 -1
  45. package/test/browser/screen.check.mjs +142 -11
  46. package/test/browser/serve-response.mjs +5 -2
  47. package/test/browser/side-panel-sweep.check.mjs +171 -10
  48. package/test/data-motion.test.ts +39 -8
  49. package/test/fasten-form.test.ts +25 -0
  50. package/test/fasten-geometry.test.ts +13 -0
  51. package/translations/en.json +23 -11
  52. package/translations/es.json +22 -10
  53. package/translations/ja.json +22 -10
  54. package/translations/ko.json +24 -12
  55. package/translations/ms.json +22 -10
  56. package/translations/zh.json +22 -10
@@ -1,14 +1,16 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import '@material/web/icon/icon.js';
3
3
  import { css, html, LitElement, nothing } from 'lit';
4
+ import JSON5 from 'json5';
5
+ import { ScrollbarStyles } from '@operato/styles';
4
6
  import { customElement, property, state } from 'lit/decorators.js';
5
7
  import { i18next, localize } from '@operato/i18n';
6
8
  import { unitWord } from './unit-words.js';
7
9
  import { FIGURE_UNITS, figureDataExample, inferFigureDataSchema } from '@hatiolab/figure-model';
8
- import { accessorOfRow, behaviorsOf, bindingDraftOf, domainOf, endlessJointsOf, fieldAt, fieldOfKind, fitsField, freeCodeOf, legendRoleOf, methodOf, methodsOf, partsOf, rotationStepsOf, sampleOf, schemaRowsOf, schemaWithField, schemaWithFieldReplaced, schemaWithoutField, schemaFollowingCodes, stateInputsOf, tableColumnsOf, tableDraftOf, tableWithKeys, tableWithLook, tableWithTargets } from './data-interface.js';
10
+ import { accessorOfRow, behaviorsOf, bindingDraftOf, domainOf, endlessJointsOf, fieldAt, fieldOfKind, fitsField, freeCodeOf, legendRoleOf, methodOf, methodsOf, freeNumberCodeOf, tableWithKeep, partsOf, rotationStepsOf, sampleOf, schemaRowsOf, schemaWithField, schemaWithFieldReplaced, schemaWithoutField, schemaFollowingCodes, stateInputsOf, tableColumnsOf, tableDraftOf, tableWithKeys, tableWithLook, tableWithTargets } from './data-interface.js';
9
11
  import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js';
10
12
  import { dataMoveOf } from '@things-factory/figure-viewer/dist-client/data-connection.js';
11
- import { kindWords, moveWords } from '@things-factory/figure-viewer/dist-client/data-words.js';
13
+ import { kindName, kindWords, moveWords } from '@things-factory/figure-viewer/dist-client/data-words.js';
12
14
  import { figureBindingsTakenOver, figureCapabilityDescriptorOf } from '@hatiolab/things-scene';
13
15
  import { consumerDescriptors } from './consumer-capabilities.js';
14
16
  import { HelpToggles } from './help.js';
@@ -25,6 +27,8 @@ const KINDS = ['number', 'boolean', 'text', 'object', 'list'];
25
27
  * running on it (`data-sample`); a sample legend pasted for a table's role shows it as a board's legend would
26
28
  * (`legend-sample`).
27
29
  */
30
+ /** A binding being added to the open field: nothing chosen yet. */
31
+ const NO_CONNECTING = { method: '', behavior: '', parts: [], inputs: [], group: '' };
28
32
  let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement) {
29
33
  constructor() {
30
34
  super(...arguments);
@@ -32,13 +36,15 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
32
36
  this.locked = false;
33
37
  /** The part chosen in the modeller, put among a table's parts by one button. */
34
38
  this.selected = '';
39
+ /** Whether the editor shows the preview: the sample shows there only, the edit canvas keeping the figure as made. */
40
+ this.previewing = false;
35
41
  /** The schema row chosen, by its path; its field is edited under it. */
36
42
  this.chosen = '';
37
43
  this.adding = { name: '', kind: 'number' };
38
44
  /** A site's data pasted to start the schema from; nothing is kept until the author says so. */
39
45
  this.pasted = '';
40
46
  /** A binding as it is being added: the field (or a new field's name), the method, the behavior or the table's targets. */
41
- this.connecting = { path: '', name: '', method: '', behavior: '', parts: [], inputs: [], group: '' };
47
+ this.connecting = { ...NO_CONNECTING };
42
48
  /** The binding opened to edit its rule, by id. */
43
49
  this.opened = '';
44
50
  /** Sample legends by role, as JSON typed; not saved. */
@@ -50,45 +56,87 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
50
56
  this.sampling = false;
51
57
  }
52
58
  static { this.styles = [
59
+ /* The panel's scrollbars -- the sample's box among them -- in the shell's look, not the browser's. */
60
+ ScrollbarStyles,
53
61
  SidePanelStyles,
54
62
  css `
55
63
  :host {
56
64
  display: block;
57
65
  }
58
- ul[tree] {
59
- display: grid;
60
- gap: 2px;
61
- margin: 0;
62
- padding: 0;
63
- list-style: none;
64
- }
65
- ul[tree] > li > button[row] {
66
+ /* Where the sample shows, said while editing, with the press that goes there. */
67
+ p[sample-where] {
66
68
  display: flex;
67
69
  flex-wrap: wrap;
68
- gap: 0 6px;
69
- width: 100%;
70
+ align-items: center;
71
+ gap: 4px 8px;
72
+ margin: 4px 0 0;
73
+ font-size: var(--ui-t-small, 11.5px);
74
+ color: var(--md-sys-color-on-surface-variant);
75
+ }
76
+ /*
77
+ The fields as a table -- field, kind, what it moves -- a line between rows: a list of two-line names with their
78
+ kinds pushed right ran together, one field not told from the next (user 2026-11-12).
79
+ */
80
+ div[fields] {
81
+ display: grid;
82
+ grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr);
83
+ border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
84
+ }
85
+ div[fields] > div[head],
86
+ div[fields] > button[row] {
87
+ grid-column: 1 / -1;
88
+ display: grid;
89
+ grid-template-columns: subgrid;
90
+ column-gap: 8px;
91
+ align-items: center;
92
+ padding: 5px 6px;
93
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
94
+ }
95
+ div[fields] > div[head] {
96
+ font-size: var(--ui-t-micro, 11px);
97
+ color: var(--md-sys-color-on-surface-variant);
98
+ }
99
+ div[fields] > button[row] {
70
100
  min-height: var(--figure-control-height);
71
101
  margin: 0;
72
- padding: 4px 6px;
73
- border: 1px solid transparent;
74
- border-radius: 6px;
102
+ border-top: none;
103
+ border-left: none;
104
+ border-right: none;
75
105
  background: none;
76
106
  color: inherit;
77
107
  font: inherit;
78
108
  text-align: left;
79
- align-items: center;
80
109
  cursor: pointer;
81
110
  }
82
- ul[tree] > li > button[row][selected] {
83
- border-color: var(--md-sys-color-primary);
111
+ div[fields] > button[row]:hover {
112
+ background: var(--md-sys-color-surface-container-low, rgba(0, 0, 0, 0.03));
113
+ }
114
+ div[fields] > button[row][selected] {
115
+ background: var(--md-sys-color-secondary-container, rgba(0, 0, 0, 0.06));
116
+ }
117
+ div[fields] span[role='cell'] {
118
+ min-width: 0;
119
+ overflow-wrap: anywhere;
84
120
  }
85
- ul[tree] > li > button[row] small {
121
+ div[fields] span[name] {
122
+ display: grid;
123
+ }
124
+ div[fields] span[name] small,
125
+ div[fields] span[none] {
86
126
  color: var(--md-sys-color-on-surface-variant);
87
127
  }
88
- /* What the field is and what reads it: beside the name when it fits, under it when not. */
89
- ul[tree] > li > button[row] small[meta] {
90
- margin-left: auto;
91
- text-align: right;
128
+ div[fields] div[bindings] > h4 {
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 4px;
132
+ margin: 12px 0 6px;
133
+ font-size: var(--ui-t-body, 13px);
134
+ font-weight: 600;
135
+ }
136
+ div[fields] > div[detail] {
137
+ grid-column: 1 / -1;
138
+ padding: 8px 6px;
139
+ border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
92
140
  }
93
141
  section[item] {
94
142
  display: grid;
@@ -132,18 +180,6 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
132
180
  margin: 0;
133
181
  padding: 0;
134
182
  }
135
- div[cases] {
136
- display: grid;
137
- grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
138
- gap: 4px 6px;
139
- align-items: center;
140
- }
141
- div[codes] {
142
- display: grid;
143
- grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 30px;
144
- gap: 4px 6px;
145
- align-items: center;
146
- }
147
183
  div[codes] button[act] {
148
184
  width: 30px;
149
185
  margin: 0;
@@ -161,52 +197,132 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
161
197
  padding: 0;
162
198
  }
163
199
  /* A table's rows: the key (a code, or from and to), a cell per column, and a remove button. */
164
- div[looks] {
200
+ /* A table's values, a box each: the value on the head line, its settings a property grid under it (user 2026-11-12). */
201
+ div[cases] {
165
202
  display: grid;
166
- gap: 4px 6px;
203
+ gap: 8px;
204
+ }
205
+ div[case] {
206
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
207
+ border-radius: 6px;
208
+ overflow: hidden;
209
+ }
210
+ div[case-head] {
211
+ display: flex;
167
212
  align-items: center;
168
- overflow-x: auto;
213
+ gap: 6px;
214
+ min-height: var(--figure-control-height);
215
+ padding: 2px 6px;
216
+ background: var(--md-sys-color-surface-container-low, rgba(0, 0, 0, 0.03));
169
217
  }
170
- div[looks] > small {
218
+ div[case-head] small {
171
219
  color: var(--md-sys-color-on-surface-variant);
172
220
  }
173
- div[looks] input,
174
- div[looks] select {
221
+ div[case-head] input {
222
+ flex: 1;
175
223
  min-width: 0;
176
- width: 100%;
177
224
  box-sizing: border-box;
178
225
  }
179
- div[looks] button[act] {
180
- width: 30px;
226
+ div[case-head] button[act] {
227
+ margin-left: auto;
228
+ }
229
+ div[case] > p[hint] {
181
230
  margin: 0;
182
- padding: 0;
231
+ padding: 6px;
232
+ border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
233
+ }
234
+ /* The 「그대로 둠」 of the other-values box: a checkbox and its word beside the box's name. */
235
+ div[case-head] label[keep] {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 4px;
239
+ font-size: var(--ui-t-small, 11.5px);
240
+ color: var(--md-sys-color-on-surface-variant);
241
+ }
242
+ div[case-head] label[keep] input {
243
+ flex: none;
244
+ width: auto;
245
+ margin: 0;
246
+ }
247
+ /* A property grid: a line a setting, its name left, its field right, a line between and between the two. */
248
+ div[props] {
249
+ display: grid;
250
+ grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
251
+ }
252
+ div[props] > span {
253
+ display: flex;
254
+ align-items: center;
255
+ min-width: 0;
256
+ min-height: var(--figure-control-height);
257
+ padding: 2px 6px;
258
+ border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
259
+ }
260
+ div[props] > span:nth-child(odd) {
261
+ border-right: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
262
+ }
263
+ div[props] > span:nth-child(odd) small {
264
+ overflow: hidden;
265
+ color: var(--md-sys-color-on-surface-variant);
266
+ white-space: nowrap;
267
+ text-overflow: ellipsis;
268
+ }
269
+ div[props] input,
270
+ div[props] select {
271
+ width: 100%;
272
+ min-width: 0;
273
+ box-sizing: border-box;
183
274
  }
184
275
  /* A colour and the button that clears it, on one line at the height of the cells beside it. */
185
- div[looks] span[colour] {
276
+ div[props] span[colour] {
186
277
  display: flex;
278
+ flex: 1;
187
279
  gap: 2px;
188
280
  align-items: center;
189
281
  min-width: 0;
190
282
  }
191
- div[looks] span[colour] input[type='color'] {
283
+ div[props] span[colour] input[type='color'] {
192
284
  flex: 1;
193
285
  height: var(--figure-control-height);
194
286
  padding: 0 2px;
195
287
  }
196
- div[looks] span[colour] button[act] {
288
+ div[props] span[colour] button[act] {
197
289
  flex: none;
198
290
  width: 24px;
199
291
  }
200
- div[chips] {
201
- display: flex;
202
- flex-wrap: wrap;
203
- gap: 4px 10px;
292
+ /* A table's targets as a property list: what each is, its name, its delete. */
293
+ div[targets] {
294
+ display: grid;
295
+ grid-template-columns: auto minmax(0, 1fr) auto;
204
296
  align-items: center;
297
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
298
+ border-radius: 6px;
299
+ overflow: hidden;
205
300
  }
206
- div[chips] label {
207
- display: inline-flex;
208
- gap: 4px;
209
- align-items: center;
301
+ div[targets] > * {
302
+ min-width: 0;
303
+ padding: 2px 6px;
304
+ border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
305
+ }
306
+ div[targets] > small[head] {
307
+ grid-column: 1 / -1;
308
+ border-top: none;
309
+ padding: 4px 6px;
310
+ color: var(--md-sys-color-on-surface-variant);
311
+ background: var(--md-sys-color-surface-container-low, rgba(0, 0, 0, 0.03));
312
+ }
313
+ div[targets] > span:not([none]):nth-of-type(odd) {
314
+ color: var(--md-sys-color-on-surface-variant);
315
+ font-size: var(--ui-t-small, 11.5px);
316
+ }
317
+ div[targets] > span[none] {
318
+ grid-column: 1 / -1;
319
+ color: var(--md-sys-color-on-surface-variant);
320
+ }
321
+ div[targets] > button[act] {
322
+ margin: 0;
323
+ padding: 0 4px;
324
+ border: none;
325
+ border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
210
326
  }
211
327
  textarea {
212
328
  width: 100%;
@@ -214,10 +330,11 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
214
330
  box-sizing: border-box;
215
331
  font-family: var(--ui-mono, ui-monospace, monospace);
216
332
  font-size: inherit;
333
+ resize: vertical;
217
334
  }
218
- ul[result] {
219
- margin: 0;
220
- padding-left: 16px;
335
+ /* The sample is read whole, a record of a dozen lines: room for it, not six lines of it (user 2026-11-12). */
336
+ textarea[sample] {
337
+ min-height: 240px;
221
338
  }
222
339
  md-icon {
223
340
  font-size: 16px;
@@ -232,7 +349,7 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
232
349
  return nothing;
233
350
  const rows = schemaRowsOf(asset);
234
351
  return html `
235
- ${this.renderCapabilityData(asset)} ${this.renderSchema(asset, rows)} ${this.renderBindings(asset, rows)} ${rows.length ? this.renderSample(asset) : nothing}
352
+ ${this.renderCapabilityData(asset)} ${this.renderSchema(asset, rows)} ${rows.length ? this.renderSample(asset) : nothing}
236
353
  `;
237
354
  }
238
355
  /** The data a product's capability the figure wears reads itself (ADR-0167 14): said, read only. */
@@ -248,27 +365,43 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
248
365
  /* ---------------------------------------------------------------- the schema */
249
366
  renderSchema(asset, rows) {
250
367
  const schema = asset.dataInterface?.schema ?? [];
368
+ const bindings = asset.dataInterface?.bindings ?? [];
369
+ const choices = behaviorsOf(asset);
370
+ const resting = figureBindingsTakenOver(asset);
251
371
  return html `<section>
252
372
  <h3>${i18next.t('figure.label.data-schema')}${this.help.button('figure.text.data-schema-explained')}</h3>
253
373
  ${this.help.hint('figure.text.data-schema-explained')}
254
374
  ${rows.length
255
- ? html `<ul tree>
256
- ${rows.map(row => html `<li>
257
- <button row ?selected=${row.path === this.chosen} style="padding-left: ${6 + row.depth * 14}px" @click=${() => (this.chosen = row.path === this.chosen ? '' : row.path)}>
258
- <span>${row.name}${row.label ? html ` <small>${row.label}</small>` : nothing}</span>
259
- <small meta>${this.kindWord(row.node)}${row.bindings.length ? ` · ${i18next.t('figure.label.data-bound', { count: row.bindings.length })}` : ''}</small>
375
+ ? html `<div fields role="table">
376
+ <div head role="row">
377
+ <span role="columnheader">${i18next.t('figure.label.data-column-field')}</span>
378
+ <span role="columnheader">${i18next.t('figure.label.data-column-kind')}</span>
379
+ <span role="columnheader">${i18next.t('figure.label.data-column-moves')}</span>
380
+ </div>
381
+ ${rows.map(row => {
382
+ /* What the field moves, named, in place of a count of bindings (「작동 1개」 said nothing, user 2026-11-12). */
383
+ const moves = row.bindings.map(id => bindings.find(b => b.id === id)).filter((b) => !!b).map(b => this.bindingWhat(asset, b));
384
+ return html `<button
385
+ row
386
+ role="row"
387
+ ?selected=${row.path === this.chosen}
388
+ @click=${() => {
389
+ this.chosen = row.path === this.chosen ? '' : row.path;
390
+ this.connecting = { ...NO_CONNECTING };
391
+ }}
392
+ >
393
+ <span name role="cell" style="padding-left: ${row.depth * 14}px"><span>${row.name}</span>${row.label ? html `<small>${row.label}</small>` : nothing}</span>
394
+ <span role="cell" title=${kindWords(row.node)}>${kindName(row.node)}</span>
395
+ <span role="cell" ?none=${!moves.length}>${moves.length ? moves.join(', ') : '—'}</span>
260
396
  </button>
261
- ${row.path === this.chosen ? this.renderField(asset, schema, row) : nothing}
262
- </li>`)}
263
- </ul>`
397
+ ${row.path === this.chosen ? html `<div detail>${this.renderField(asset, schema, row)} ${this.renderFieldBindings(asset, row, choices, resting)}</div>` : nothing}`;
398
+ })}
399
+ </div>`
264
400
  : html `<p hint>${i18next.t('figure.text.data-no-fields')}</p>`}
401
+ ${this.renderStrayBindings(asset, rows, choices, resting)} ${this.renderRotation(asset, rows)}
265
402
  ${this.renderAddField(schema, '')} ${this.renderFromSample(schema)}
266
403
  </section>`;
267
404
  }
268
- /** A field's kind in the words every screen says it in (figure-viewer kindWords). */
269
- kindWord(node) {
270
- return kindWords(node);
271
- }
272
405
  /** The chosen field's own settings, and a field added under it when it holds fields. */
273
406
  renderField(asset, schema, row) {
274
407
  const field = fieldAt(schema, row.path);
@@ -379,7 +512,14 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
379
512
  this.edit({ kind: 'set-data-schema', schema: schemaWithField(schema, under, { name: named, ...nodeOfKind(a.kind) }) });
380
513
  this.adding = { ...a, name: '' };
381
514
  };
382
- return html `<div form>
515
+ const words = i18next.t(under ? 'figure.button.add-data-child' : 'figure.button.add-data-field', { parent: under });
516
+ /*
517
+ Folded under its summary, as a binding's form is: two fields always open under the table read as one more row of
518
+ it (user 2026-11-12). Open while the figure has no field yet: adding one is the only thing to do.
519
+ */
520
+ return html `<details add-field ?open=${!schema.length}>
521
+ <summary>${words}</summary>
522
+ <div form>
383
523
  <span>${i18next.t('figure.label.data-name')}</span>
384
524
  <input
385
525
  aria-label=${i18next.t('figure.label.data-name')}
@@ -393,8 +533,9 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
393
533
  </select>
394
534
  </div>
395
535
  <button act ?disabled=${this.locked || !named} @click=${add}>
396
- <md-icon>add</md-icon>${i18next.t(under ? 'figure.button.add-data-child' : 'figure.button.add-data-field', { parent: under })}
397
- </button>`;
536
+ <md-icon>add</md-icon>${i18next.t('figure.button.add')}
537
+ </button>
538
+ </details>`;
398
539
  }
399
540
  /** A site's data pasted: the schema guessed from it, kept only when the author says so -- units, ranges and closed values are theirs to set. */
400
541
  renderFromSample(schema) {
@@ -402,7 +543,7 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
402
543
  let unreadable = false;
403
544
  if (this.pasted.trim()) {
404
545
  try {
405
- guessed = inferFigureDataSchema(JSON.parse(this.pasted));
546
+ guessed = inferFigureDataSchema(JSON5.parse(this.pasted));
406
547
  }
407
548
  catch {
408
549
  unreadable = true;
@@ -427,32 +568,51 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
427
568
  </details>`;
428
569
  }
429
570
  /* ---------------------------------------------------------------- the bindings */
571
+ /** What a binding moves: a table's parts, a value's behavior -- one wording for the binding's line and the field's row. */
572
+ bindingWhat(asset, b) {
573
+ return moveWords(dataMoveOf(asset, b.rule === 'table' ? { kind: 'table', targets: b.targets } : b.behavior));
574
+ }
430
575
  /** What a behavior moves, in the words every screen says it in (figure-viewer dataMoveOf · moveWords). */
431
576
  behaviorName(asset, b) {
432
577
  return moveWords(dataMoveOf(asset, b));
433
578
  }
434
- renderBindings(asset, rows) {
435
- const bindings = asset.dataInterface?.bindings ?? [];
436
- const choices = behaviorsOf(asset);
437
- const resting = figureBindingsTakenOver(asset);
438
- return html `<section>
439
- <h3>${i18next.t('figure.label.data-bindings')}${this.help.button('figure.text.data-bindings-explained')}</h3>
579
+ /*
580
+ A field's bindings under the field, in its opened row: what the field moves and how, edited where the field is. A
581
+ list of bindings of its own below the fields said every field's name and move again (user 2026-11-12).
582
+ */
583
+ renderFieldBindings(asset, row, choices, resting) {
584
+ const bindings = (asset.dataInterface?.bindings ?? []).filter(b => row.bindings.includes(b.id));
585
+ const bindable = row.node.kind !== 'object';
586
+ if (!bindable && !bindings.length)
587
+ return nothing;
588
+ return html `<div bindings>
589
+ <h4>${i18next.t('figure.label.data-bindings-title')}${this.help.button('figure.text.data-bindings-explained')}</h4>
440
590
  ${this.help.hint('figure.text.data-bindings-explained')}
441
591
  ${bindings.map(b => this.renderBinding(asset, choices, b, resting))}
442
- ${this.renderAddBinding(asset, rows, choices)}
443
- ${this.renderRotation(asset, rows)}
444
- </section>`;
592
+ ${bindable ? this.renderAddBinding(asset, row, choices) : nothing}
593
+ </div>`;
594
+ }
595
+ /** A binding no field reads -- its field renamed or taken out -- kept in sight below the fields, to delete or fix. */
596
+ renderStrayBindings(asset, rows, choices, resting) {
597
+ const read = new Set(rows.flatMap(r => r.bindings));
598
+ const stray = (asset.dataInterface?.bindings ?? []).filter(b => !read.has(b.id));
599
+ if (!stray.length)
600
+ return nothing;
601
+ return html `<div bindings>
602
+ <h4>${i18next.t('figure.label.data-bindings-without-field')}</h4>
603
+ ${stray.map(b => this.renderBinding(asset, choices, b, resting))}
604
+ </div>`;
445
605
  }
446
606
  renderBinding(asset, choices, b, resting) {
447
607
  const open = this.opened === b.id;
448
608
  const set = (next) => this.edit({ kind: 'set-data-binding', binding: next });
449
- const what = moveWords(dataMoveOf(asset, b.rule === 'table' ? { kind: 'table', targets: b.targets } : b.behavior));
609
+ const what = this.bindingWhat(asset, b);
450
610
  const rests = resting.some(r => r.id === b.id);
451
611
  return html `<section item>
452
612
  <div head>
453
613
  <button line aria-expanded=${open ? 'true' : 'false'} @click=${() => (this.opened = open ? '' : b.id)}>
454
614
  <strong>${b.label ?? b.accessor}</strong> → ${what}
455
- <small>${i18next.t('figure.label.data-method-' + methodOf(b))}${rests ? html ` <span rests>${i18next.t('figure.label.data-binding-resting')}</span>` : nothing}</small>
615
+ <small>${i18next.t('figure.text.data-method-does-' + methodOf(b))}${rests ? html ` <span rests>${i18next.t('figure.label.data-binding-resting')}</span>` : nothing}</small>
456
616
  </button>
457
617
  <button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-binding')} @click=${() => this.edit({ kind: 'remove-data-binding', id: b.id })}>
458
618
  <md-icon>delete</md-icon>
@@ -556,36 +716,61 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
556
716
  <p hint>${i18next.t('figure.text.data-legend-explained')}</p>`
557
717
  : nothing}`;
558
718
  }
559
- /** Which parts, controls and motion group a table shows on: chips to tick, and the part chosen in the modeller by one button. */
719
+ /*
720
+ Which parts, controls and motion group a table shows on, as a property list: a row a target chosen -- what it is,
721
+ its name, its delete -- and one choice to add the next. Every part and control as a tick box in one heap took two
722
+ screens and hid which were chosen (user 2026-11-12).
723
+ */
560
724
  renderTargets(parts, inputs, groups, t, on) {
561
- const chosen = (e) => e.target.checked;
562
- return html `<div form>
563
- <span>${i18next.t('figure.label.data-target-parts')}</span>
564
- <div chips>
565
- ${parts.map(p => html `<label><input type="checkbox" .checked=${!!t.parts?.includes(p)} ?disabled=${this.locked} @change=${(e) => on.part(p, chosen(e))} />${p}</label>`)}
566
- ${this.selected && parts.includes(this.selected) && !t.parts?.includes(this.selected)
567
- ? html `<button act ?disabled=${this.locked} @click=${() => on.part(this.selected, true)}><md-icon>add</md-icon>${i18next.t('figure.button.data-add-selected-part', { part: this.selected })}</button>`
568
- : nothing}
725
+ const chosenParts = t.parts ?? [];
726
+ const chosenInputs = t.inputs ?? [];
727
+ const freeParts = parts.filter(p => !chosenParts.includes(p));
728
+ const freeInputs = inputs.filter(i => !chosenInputs.includes(i.id));
729
+ const labelOf = (id) => inputs.find(i => i.id === id)?.label ?? id;
730
+ const row = (kind, name, remove) => html `<span>${i18next.t(kind)}</span><span>${name}</span>
731
+ <button act ?disabled=${this.locked} title=${i18next.t('figure.button.data-remove-target')} @click=${remove}><md-icon>delete</md-icon></button>`;
732
+ const add = (e) => {
733
+ const select = e.target;
734
+ const [kind, id] = [select.value.slice(0, select.value.indexOf(':')), select.value.slice(select.value.indexOf(':') + 1)];
735
+ select.value = '';
736
+ if (kind === 'part')
737
+ on.part(id, true);
738
+ else if (kind === 'input')
739
+ on.input(id, true);
740
+ };
741
+ return html `<div targets>
742
+ <small head>${i18next.t('figure.label.data-targets')}</small>
743
+ ${chosenParts.map(p => row('figure.label.data-target-parts', p, () => on.part(p, false)))}
744
+ ${chosenInputs.map(id => row('figure.label.data-target-inputs', labelOf(id), () => on.input(id, false)))}
745
+ ${chosenParts.length || chosenInputs.length ? nothing : html `<span none>${i18next.t('figure.text.data-no-targets')}</span>`}
569
746
  </div>
570
- ${inputs.length
571
- ? html `<span>${i18next.t('figure.label.data-target-inputs')}</span>
572
- <div chips>
573
- ${inputs.map(i => html `<label><input type="checkbox" .checked=${!!t.inputs?.includes(i.id)} ?disabled=${this.locked} @change=${(e) => on.input(i.id, chosen(e))} />${i.label}</label>`)}
574
- </div>`
747
+ <div form>
748
+ <span>${i18next.t('figure.label.data-target-add')}</span>
749
+ <select ?disabled=${this.locked || !(freeParts.length + freeInputs.length)} .value=${''} @change=${add}>
750
+ <option value="">${i18next.t('figure.label.data-target-choose')}</option>
751
+ ${freeParts.length
752
+ ? html `<optgroup label=${i18next.t('figure.label.data-target-parts')}>${freeParts.map(p => html `<option value=${'part:' + p}>${p}</option>`)}</optgroup>`
753
+ : nothing}
754
+ ${freeInputs.length
755
+ ? html `<optgroup label=${i18next.t('figure.label.data-target-inputs')}>${freeInputs.map(i => html `<option value=${'input:' + i.id}>${i.label}</option>`)}</optgroup>`
756
+ : nothing}
757
+ </select>
758
+ ${this.selected && freeParts.includes(this.selected)
759
+ ? html `<span></span><button act ?disabled=${this.locked} @click=${() => on.part(this.selected, true)}><md-icon>add</md-icon>${i18next.t('figure.button.data-add-selected-part', { part: this.selected })}</button>`
575
760
  : nothing}
576
- ${groups.length
761
+ ${groups.length
577
762
  ? html `<span>${i18next.t('figure.label.data-target-group')}</span>
578
- <select ?disabled=${this.locked} @change=${(e) => on.group(e.target.value)}>
579
- <option value="" ?selected=${!t.group}>${i18next.t('figure.label.data-none')}</option>
580
- ${groups.map(g => html `<option value=${g.id} ?selected=${g.id === t.group}>${g.label}</option>`)}
581
- </select>`
763
+ <select ?disabled=${this.locked} @change=${(e) => on.group(e.target.value)}>
764
+ <option value="" ?selected=${!t.group}>${i18next.t('figure.label.data-none')}</option>
765
+ ${groups.map(g => html `<option value=${g.id} ?selected=${g.id === t.group}>${g.label}</option>`)}
766
+ </select>`
582
767
  : nothing}
583
- </div>`;
768
+ </div>`;
584
769
  }
585
770
  /** The column heads of a table's rows, after the key's own one or two. */
586
771
  columnHeads(columns) {
587
772
  return columns.map(c => c.kind === 'value'
588
- ? html `<small>${c.label}${c.unit ? ` (${c.unit}${c.min !== undefined && c.max !== undefined ? ` ${c.min}~${c.max}` : ''})` : ''}</small>`
773
+ ? html `<small>${c.label}</small>`
589
774
  : html `<small>${i18next.t('figure.label.data-column-' + c.kind)}</small>`);
590
775
  }
591
776
  /** One look's cells, a column each: a colour, an effect, a control's value, a clip, a speed, a pause -- empty leaves the target as it is. */
@@ -607,7 +792,9 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
607
792
  </select>`;
608
793
  if (c.kind === 'value') {
609
794
  const v = look.values?.[c.id];
610
- return html `<input type="number" step="any" min=${c.min ?? ''} max=${c.max ?? ''} .value=${v === undefined ? '' : String(v)} ?disabled=${this.locked} @change=${(e) => {
795
+ /* Its unit and range in the field, not beside the name: 「fl-hip (deg -30~30)」 took two lines (user 2026-11-12). */
796
+ const range = [c.min !== undefined && c.max !== undefined ? `${c.min}~${c.max}` : '', c.unit ?? ''].filter(Boolean).join(' ');
797
+ return html `<input type="number" step="any" min=${c.min ?? ''} max=${c.max ?? ''} placeholder=${range} title=${range} aria-label=${c.label} .value=${v === undefined ? '' : String(v)} ?disabled=${this.locked} @change=${(e) => {
611
798
  const n = e.target.valueAsNumber;
612
799
  const values = { ...look.values };
613
800
  if (Number.isNaN(n))
@@ -633,16 +820,32 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
633
820
  </select>`;
634
821
  });
635
822
  }
823
+ /*
824
+ One value's settings as a property grid, a line a setting -- colour, effect, a control's value, clip, speed, pause --
825
+ its name left, its field right. A column a setting ran wider than the panel as controls were added (user 2026-11-12).
826
+ */
827
+ renderLook(asset, columns, look, write) {
828
+ if (!columns.length)
829
+ return html `<p hint>${i18next.t('figure.text.data-table-needs-target')}</p>`;
830
+ const heads = this.columnHeads(columns);
831
+ const cells = this.lookCells(asset, columns, look, write);
832
+ return html `<div props>${columns.map((_, i) => html `<span>${heads[i]}</span><span>${cells[i]}</span>`)}</div>`;
833
+ }
636
834
  /** The look of every other value: a row like the others, added or taken away; kept when a closed field's values are not all rows. */
637
- renderOtherwise(asset, b, columns, needed, set, keyCells) {
835
+ renderOtherwise(asset, b, columns, needed, set) {
638
836
  const look = b.table.default;
639
837
  if (look === undefined)
640
838
  return html `<button act ?disabled=${this.locked} @click=${() => set(tableWithLook(b, 'default', {}))}><md-icon>add</md-icon>${i18next.t('figure.button.add-data-default')}</button>`;
641
839
  const { default: _, ...rest } = b.table;
642
- return html `<div looks style="grid-template-columns: ${gridOf(keyCells, columns.length)}">
643
- <span style="grid-column: span ${keyCells}">${i18next.t('figure.label.data-otherwise')}</span>
644
- ${this.lookCells(asset, columns, look, l => set(tableWithLook(b, 'default', l)))}
645
- ${needed ? html `<span></span>` : html `<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => set({ ...b, table: rest })}><md-icon>delete</md-icon></button>`}
840
+ /* 「그대로 둠」 (ADR-0166 amendment 1): a value the table has no row for leaves the targets as it last showed them. */
841
+ const keep = look.keep === true;
842
+ return html `<div case>
843
+ <div case-head>
844
+ <strong>${i18next.t('figure.label.data-otherwise')}</strong>
845
+ <label keep><input type="checkbox" .checked=${keep} ?disabled=${this.locked} @change=${(e) => set(tableWithKeep(b, e.target.checked))} />${i18next.t('figure.label.data-keep')}</label>
846
+ ${needed ? nothing : html `<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => set({ ...b, table: rest })}><md-icon>delete</md-icon></button>`}
847
+ </div>
848
+ ${keep ? html `<p hint>${i18next.t('figure.text.data-keep')}</p>` : this.renderLook(asset, columns, look, l => set(tableWithLook(b, 'default', l)))}
646
849
  </div>`;
647
850
  }
648
851
  /**
@@ -653,6 +856,8 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
653
856
  const columns = tableColumnsOf(asset, b);
654
857
  const node = fieldAt(asset.dataInterface?.schema ?? [], b.accessor.replace(/\[\d+\]/g, '[]'));
655
858
  const boolean = node?.kind === 'boolean';
859
+ /* A number's codes are numbers, as its data gives them (ADR-0166 amendment 1). */
860
+ const numeric = node?.kind === 'number';
656
861
  const rows = Object.entries(b.table).filter(([k]) => k !== 'default');
657
862
  const domain = node ? domainOf(node).map(String) : [];
658
863
  const covered = domain.length > 0 && domain.every(v => b.table[v] !== undefined);
@@ -664,25 +869,30 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
664
869
  };
665
870
  const rekey = (i, typed) => {
666
871
  const key = typed.trim();
667
- if (!key || key === 'default' || rows.some(([k], j) => j !== i && k === key))
872
+ if (!key || key === 'default' || rows.some(([k], j) => j !== i && k === key) || (numeric && !Number.isFinite(Number(key))))
668
873
  return this.requestUpdate();
669
874
  rekeyed(rows.map(([k, v], j) => (j === i ? [key, v] : [k, v])));
670
875
  };
671
876
  const word = (k) => (boolean ? i18next.t(k === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : k);
672
- return html `<div looks style="grid-template-columns: ${gridOf(1, columns.length)}">
673
- <small>${i18next.t('figure.label.data-case-value')}</small>${this.columnHeads(columns)}<span></span>
674
- ${rows.map(([k, look], i) => html `${boolean
675
- ? html `<span>${word(k)}</span>`
676
- : html `<input .value=${k} aria-label=${i18next.t('figure.label.data-case-value')} ?disabled=${this.locked} @change=${(e) => rekey(i, e.target.value)} />`}
677
- ${this.lookCells(asset, columns, look, l => set(tableWithLook(b, k, l)))}
678
- ${boolean || rows.length < 2 && domain.length > 0
679
- ? html `<span></span>`
680
- : html `<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => rekeyed(rows.filter((_, j) => j !== i))}><md-icon>delete</md-icon></button>`}`)}
877
+ /* A value a box: its code on the box's head line, its settings a property grid under it. */
878
+ return html `<div cases>
879
+ ${rows.map(([k, look], i) => html `<div case>
880
+ <div case-head>
881
+ <small>${i18next.t('figure.label.data-case-value')}</small>
882
+ ${boolean
883
+ ? html `<strong>${word(k)}</strong>`
884
+ : html `<input .value=${k} inputmode=${numeric ? 'decimal' : 'text'} aria-label=${i18next.t('figure.label.data-case-value')} ?disabled=${this.locked} @change=${(e) => rekey(i, e.target.value)} />`}
885
+ ${boolean || (rows.length < 2 && domain.length > 0)
886
+ ? nothing
887
+ : html `<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => rekeyed(rows.filter((_, j) => j !== i))}><md-icon>delete</md-icon></button>`}
888
+ </div>
889
+ ${this.renderLook(asset, columns, look, l => set(tableWithLook(b, k, l)))}
890
+ </div>`)}
891
+ ${this.renderOtherwise(asset, b, columns, domain.length > 0 && !covered, set)}
681
892
  </div>
682
- ${this.renderOtherwise(asset, b, columns, domain.length > 0 && !covered, set, 1)}
683
893
  ${boolean
684
894
  ? nothing
685
- : html `<button act ?disabled=${this.locked} @click=${() => rekeyed([...rows, [freeCodeOf(rows.map(([k]) => k)), {}]])}>
895
+ : html `<button act ?disabled=${this.locked} @click=${() => rekeyed([...rows, [(numeric ? freeNumberCodeOf : freeCodeOf)(rows.map(([k]) => k)), {}]])}>
686
896
  <md-icon>add</md-icon>${i18next.t('figure.button.add-data-case')}
687
897
  </button>`}`;
688
898
  }
@@ -713,17 +923,21 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
713
923
  const last = rows.at(-1);
714
924
  const lastEnd = last ? ends(last[0]).to : '';
715
925
  const open = !!last && lastEnd === '';
716
- return html `<div looks style="grid-template-columns: ${gridOf(2, columns.length)}">
717
- <small>${i18next.t('figure.label.data-range-from')}</small><small>${i18next.t('figure.label.data-range-to')}</small>${this.columnHeads(columns)}<span></span>
926
+ return html `<div cases>
718
927
  ${rows.map(([k, look], i) => {
719
928
  const e = ends(k);
720
- return html `<input type="number" step="any" .value=${e.from} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'from', ev.target.value)} />
721
- <input type="number" step="any" .value=${e.to} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'to', ev.target.value)} />
722
- ${this.lookCells(asset, columns, look, l => set(tableWithLook(b, k, l)))}
723
- <button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() => set(tableWithKeys(b, rows.filter((_, j) => j !== i)))}><md-icon>delete</md-icon></button>`;
929
+ return html `<div case>
930
+ <div case-head>
931
+ <input type="number" step="any" aria-label=${i18next.t('figure.label.data-range-from')} placeholder=${i18next.t('figure.label.data-range-from')} .value=${e.from} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'from', ev.target.value)} />
932
+ <span>~</span>
933
+ <input type="number" step="any" aria-label=${i18next.t('figure.label.data-range-to')} placeholder=${i18next.t('figure.label.data-range-to')} .value=${e.to} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'to', ev.target.value)} />
934
+ <button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() => set(tableWithKeys(b, rows.filter((_, j) => j !== i)))}><md-icon>delete</md-icon></button>
935
+ </div>
936
+ ${this.renderLook(asset, columns, look, l => set(tableWithLook(b, k, l)))}
937
+ </div>`;
724
938
  })}
939
+ ${this.renderOtherwise(asset, b, columns, false, set)}
725
940
  </div>
726
- ${this.renderOtherwise(asset, b, columns, false, set, 2)}
727
941
  ${open ? html `<p hint>${i18next.t('figure.text.data-range-close-last')}</p>` : nothing}
728
942
  <button act ?disabled=${this.locked || open} @click=${() => set(tableWithKeys(b, [...rows, [`${lastEnd}~`, {}]]))}>
729
943
  <md-icon>add</md-icon>${i18next.t('figure.button.add-data-range')}
@@ -766,27 +980,23 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
766
980
  * 「연결 추가」 (ADR-0166 10): a field there or a new name, then a method the field's kind allows. As it is or scaled,
767
981
  * the one thing it moves; a table, the parts, controls and motion group it shows on -- a new name makes the field.
768
982
  */
769
- renderAddBinding(asset, rows, choices) {
770
- const bindable = rows.filter(r => r.node.kind !== 'object');
983
+ /** A binding added to the field whose row is open: how it is read, and what it moves. */
984
+ renderAddBinding(asset, row, choices) {
771
985
  const a = this.connecting;
772
- /* 「새 필드」: a value no field path can have (a path is names and `[]`). */
773
- const NEW = '+';
774
- const row = a.path === NEW || !bindable.length ? null : (bindable.find(r => r.path === a.path) ?? bindable[0]);
775
- const methods = methodsOf(row?.node ?? null, choices);
986
+ const methods = methodsOf(row.node, choices);
776
987
  const method = methods.includes(a.method) ? a.method : methods[0];
777
988
  const table = method === 'code' || method === 'ranges';
778
- const fitting = row && !table ? choices.filter(c => fitsField(row.node, c)) : [];
989
+ const fitting = !table ? choices.filter(c => fitsField(row.node, c)) : [];
779
990
  const choice = fitting.find(c => c.key === a.behavior) ?? fitting[0];
780
991
  const set = (patch) => (this.connecting = { ...a, ...patch });
781
992
  const typed = (e) => e.target.value;
782
- const name = a.name.trim();
783
993
  const targets = { parts: a.parts, inputs: a.inputs, ...(a.group ? { group: a.group } : {}) };
784
994
  const aimed = a.parts.length > 0 || a.inputs.length > 0 || !!a.group;
785
- const ready = table ? aimed && (row !== null || /^[A-Za-z_][\w-]*$/.test(name)) : !!choice && !!row;
995
+ const ready = table ? aimed : !!choice;
786
996
  const add = () => {
787
997
  if (table) {
788
- const { binding, schema } = tableDraftOf(asset, row ? { path: row.path, node: row.node } : { name }, method, targets);
789
- this.edit({ kind: 'set-data-binding', binding, ...(schema ? { schema } : {}) });
998
+ const { binding } = tableDraftOf(asset, { path: row.path, node: row.node }, method, targets);
999
+ this.edit({ kind: 'set-data-binding', binding });
790
1000
  this.opened = binding.id;
791
1001
  }
792
1002
  else {
@@ -796,19 +1006,13 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
796
1006
  this.edit({ kind: 'set-data-binding', binding });
797
1007
  this.opened = binding.id;
798
1008
  }
799
- this.connecting = { path: '', name: '', method: '', behavior: '', parts: [], inputs: [], group: '' };
1009
+ this.connecting = { ...NO_CONNECTING };
800
1010
  };
801
1011
  const toggle = (list, id, on) => (on ? [...list, id] : list.filter(x => x !== id));
802
- return html `<div form>
803
- <span>${i18next.t('figure.label.data-field')}</span>
804
- <select @change=${(e) => set({ path: typed(e), name: '' })}>
805
- ${bindable.map(r => html `<option value=${r.path} ?selected=${r === row}>${r.path}</option>`)}
806
- <option value=${NEW} ?selected=${row === null}>${i18next.t('figure.label.data-new-field')}</option>
807
- </select>
808
- ${row === null
809
- ? html `<span>${i18next.t('figure.label.data-name')}</span>
810
- <input .value=${a.name} @input=${(e) => set({ name: typed(e) })} />`
811
- : nothing}
1012
+ /* Folded until asked for: open under a field's binding, its fields read as that binding's own (user 2026-11-12). */
1013
+ return html `<details add-binding>
1014
+ <summary>${i18next.t('figure.button.add-data-binding')}</summary>
1015
+ <div form>
812
1016
  <span>${i18next.t('figure.label.data-method')}</span>
813
1017
  ${methods.length === 1
814
1018
  ? /* One way only (a text is read by a code table): said, not offered as a choice of one (sweep 2026-10-27 U10). */
@@ -833,8 +1037,9 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
833
1037
  ${table && !aimed ? html `<p hint>${i18next.t('figure.text.data-table-needs-target')}</p>` : nothing}
834
1038
  ${methods.length ? nothing : html `<p hint>${i18next.t('figure.text.data-no-behavior')}</p>`}
835
1039
  <button act ?disabled=${this.locked || !ready} @click=${add}>
836
- <md-icon>add</md-icon>${i18next.t('figure.button.add-data-binding')}
837
- </button>`;
1040
+ <md-icon>add</md-icon>${i18next.t('figure.button.add')}
1041
+ </button>
1042
+ </details>`;
838
1043
  }
839
1044
  /**
840
1045
  * 「+ 회전」: a joint that turns without end, turned by the data -- on, speed in rpm, direction -- in one step. The
@@ -913,41 +1118,29 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
913
1118
  /* ---------------------------------------------------------------- the sample */
914
1119
  /** Data to try, read the way a placed figure reads it: what it moves, or why it is refused; in the preview, the figure running on it. */
915
1120
  renderSample(asset) {
916
- const text = this.sample || JSON.stringify(figureDataExample(asset.dataInterface), null, 2);
1121
+ /* Written and read as JSON5: names without quotes, comments and a last comma are allowed when typed by hand (user 2026-11-12). */
1122
+ const text = this.sample || JSON5.stringify(figureDataExample(asset.dataInterface), null, 2);
917
1123
  let data;
918
1124
  let unreadable = false;
919
1125
  try {
920
- data = JSON.parse(text);
1126
+ data = JSON5.parse(text);
921
1127
  }
922
1128
  catch {
923
1129
  unreadable = true;
924
1130
  }
925
1131
  const legends = this.sampleLegends();
926
1132
  const read = unreadable ? null : sampleOf(asset, data, legends);
927
- const r = read?.result;
928
1133
  const tables = (asset.dataInterface?.bindings ?? []).filter((b) => b.rule === 'table');
929
1134
  return html `<section>
930
1135
  <h3>${i18next.t('figure.label.data-sample')}${this.help.button('figure.text.data-sample-explained')}</h3>
931
1136
  ${this.help.hint('figure.text.data-sample-explained')}
932
- <textarea .value=${text} @input=${(e) => this.try(e.target.value)}></textarea>
1137
+ <textarea sample .value=${text} @input=${(e) => this.try(e.target.value)}></textarea>
933
1138
  ${unreadable
934
1139
  ? html `<p hint>${i18next.t('figure.text.data-not-json')}</p>`
935
1140
  : read.issues.length
936
1141
  ? html `<p hint>${read.issues.map(i => i18next.t('figure.text.data-value-' + i.reason, { path: i.path || '-' })).join(' · ')}</p>`
937
- : html `<ul result>
938
- ${Object.entries(r.states).map(([input, s]) => html `<li>${asset.stateInputs?.[input]?.label ?? input} = ${s.value}</li>`)}
939
- ${Object.entries(r.palette).map(([token, colour]) => html `<li>${token} = ${colour}</li>`)}
940
- ${Object.entries(r.motion).map(([group, m]) => html `<li>
941
- ${(asset.motionGroups ?? []).find(g => g.id === group)?.label ?? group}:
942
- ${[m.clip, m.speed === undefined ? '' : `× ${m.speed}`, m.paused ? i18next.t('figure.label.data-paused') : ''].filter(Boolean).join(' ')}
943
- </li>`)}
944
- ${tables.map(b => this.renderTableUse(asset, b, read.uses[b.id]))}
945
- ${Object.entries(r.parts).map(([part, look]) => html `<li>${part}: ${[look.color, look.effect ? i18next.t('figure.label.data-effect-' + look.effect.kind) : ''].filter(Boolean).join(' ')}</li>`)}
946
- ${Object.entries(r.pose).map(([axis, p]) => html `<li>${i18next.t('figure.label.data-axis-' + axis)} = ${p.value}</li>`)}
947
- ${r.items.map(i => html `<li>${i18next.t('figure.label.data-items-count', { count: i.records.length })}${i.records.length ? `: ${i.records.map(x => x.id).join(', ')}` : ''}</li>`)}
948
- </ul>`}
1142
+ : nothing}
949
1143
  ${read?.notes.length ? html `<p hint>${read.notes.map(n => i18next.t('figure.text.data-note-' + n.reason, { binding: n.binding ?? '', key: n.key ?? '', column: n.column ? i18next.t('figure.label.data-column-' + n.column) : '', value: shownValue(n.value) })).join(' · ')}</p>` : nothing}
950
- ${r && Object.keys(r.pose).length ? html `<p hint>${i18next.t('figure.text.data-sample-moves-all')}</p>` : nothing}
951
1144
  ${this.renderSampleLegends(tables)}
952
1145
  <label toggle>
953
1146
  <input
@@ -960,17 +1153,16 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
960
1153
  />
961
1154
  ${i18next.t('figure.label.data-sample-in-preview')}
962
1155
  </label>
1156
+ ${this.sampling && !this.previewing
1157
+ ? html `<p hint sample-where>
1158
+ ${i18next.t('figure.text.data-sample-shown-in-preview')}
1159
+ <button quiet @click=${() => this.dispatchEvent(new CustomEvent('show-preview', { bubbles: true, composed: true }))}>
1160
+ ${i18next.t('figure.button.data-sample-show-preview')}
1161
+ </button>
1162
+ </p>`
1163
+ : nothing}
963
1164
  </section>`;
964
1165
  }
965
- /** Which row of a table the sample's value matched, and from which table: the figure's own or the sample legend of its role. */
966
- renderTableUse(asset, b, use) {
967
- if (!use || use.value === undefined)
968
- return nothing;
969
- const word = (v) => (typeof v === 'boolean' || v === 'true' || v === 'false' ? i18next.t(String(v) === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : String(v));
970
- const row = use.key === null ? i18next.t('figure.label.data-none') : use.key === 'default' ? i18next.t('figure.label.data-otherwise') : word(use.key);
971
- const from = use.source === 'figure' ? i18next.t('figure.label.data-table-own') : i18next.t('figure.label.data-table-legend', { name: use.legend ?? '' });
972
- return html `<li>${b.label ?? b.accessor} = ${word(use.value)} → ${row} <small>(${from})</small></li>`;
973
- }
974
1166
  /** The sample legends as read: by role, those that are JSON. */
975
1167
  sampleLegends() {
976
1168
  const out = {};
@@ -978,7 +1170,7 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
978
1170
  if (!text.trim())
979
1171
  continue;
980
1172
  try {
981
- out[role] = JSON.parse(text);
1173
+ out[role] = JSON5.parse(text);
982
1174
  }
983
1175
  catch {
984
1176
  /* Not JSON yet: the figure's own table shows. */
@@ -1011,7 +1203,7 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
1011
1203
  try(typed) {
1012
1204
  this.sample = typed;
1013
1205
  try {
1014
- this.tell(JSON.parse(typed));
1206
+ this.tell(JSON5.parse(typed));
1015
1207
  }
1016
1208
  catch {
1017
1209
  /* Not JSON yet: the preview keeps what it ran on. */
@@ -1037,6 +1229,10 @@ __decorate([
1037
1229
  property(),
1038
1230
  __metadata("design:type", Object)
1039
1231
  ], FigureDataPanel.prototype, "selected", void 0);
1232
+ __decorate([
1233
+ property({ type: Boolean }),
1234
+ __metadata("design:type", Object)
1235
+ ], FigureDataPanel.prototype, "previewing", void 0);
1040
1236
  __decorate([
1041
1237
  state(),
1042
1238
  __metadata("design:type", Object)
@@ -1087,8 +1283,4 @@ function nodeOfKind(kind) {
1087
1283
  }
1088
1284
  /** A value in a sentence: a text as it is, anything else as JSON. */
1089
1285
  const shownValue = (v) => (typeof v === 'string' ? v : JSON.stringify(v ?? ''));
1090
- /** A table row's columns: the key's one or two, a cell per column, and the remove button. */
1091
- function gridOf(keyCells, columns) {
1092
- return `${'minmax(64px, 1fr) '.repeat(keyCells)}${'minmax(72px, 1fr) '.repeat(columns)}30px`;
1093
- }
1094
1286
  //# sourceMappingURL=figure-data-panel.js.map