@things-factory/figure-ui 10.1.225 → 10.1.226

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 (73) hide show
  1. package/client/modeller/consumer-capabilities.ts +15 -0
  2. package/client/modeller/editor-styles.ts +18 -1
  3. package/client/modeller/editor.ts +38 -26
  4. package/client/modeller/figure-capability-panel.ts +31 -10
  5. package/client/modeller/figure-clips.ts +2 -1
  6. package/client/modeller/figure-controls.ts +4 -3
  7. package/client/modeller/figure-data-panel.ts +24 -7
  8. package/client/modeller/figure-links.ts +5 -5
  9. package/client/modeller/figure-material-panel.ts +2 -0
  10. package/client/modeller/figure-preview.ts +69 -2
  11. package/client/modeller/figure-recipe-catalog.ts +4 -0
  12. package/client/modeller/figure-slot-layout-panel.ts +25 -8
  13. package/client/modeller/figure-verdict.ts +4 -1
  14. package/client/modeller/unit-words.ts +11 -0
  15. package/client/pages/figure-modeller-page.ts +3 -2
  16. package/dist-client/modeller/consumer-capabilities.d.ts +11 -0
  17. package/dist-client/modeller/consumer-capabilities.js +14 -0
  18. package/dist-client/modeller/consumer-capabilities.js.map +1 -1
  19. package/dist-client/modeller/editor-styles.js +18 -1
  20. package/dist-client/modeller/editor-styles.js.map +1 -1
  21. package/dist-client/modeller/editor.js +38 -26
  22. package/dist-client/modeller/editor.js.map +1 -1
  23. package/dist-client/modeller/figure-capability-panel.d.ts +3 -0
  24. package/dist-client/modeller/figure-capability-panel.js +29 -11
  25. package/dist-client/modeller/figure-capability-panel.js.map +1 -1
  26. package/dist-client/modeller/figure-clips.js +2 -1
  27. package/dist-client/modeller/figure-clips.js.map +1 -1
  28. package/dist-client/modeller/figure-controls.js +4 -3
  29. package/dist-client/modeller/figure-controls.js.map +1 -1
  30. package/dist-client/modeller/figure-data-panel.js +24 -7
  31. package/dist-client/modeller/figure-data-panel.js.map +1 -1
  32. package/dist-client/modeller/figure-links.js +5 -5
  33. package/dist-client/modeller/figure-links.js.map +1 -1
  34. package/dist-client/modeller/figure-material-panel.js +2 -0
  35. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  36. package/dist-client/modeller/figure-preview.d.ts +3 -0
  37. package/dist-client/modeller/figure-preview.js +78 -2
  38. package/dist-client/modeller/figure-preview.js.map +1 -1
  39. package/dist-client/modeller/figure-recipe-catalog.js +4 -0
  40. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -1
  41. package/dist-client/modeller/figure-slot-layout-panel.d.ts +1 -1
  42. package/dist-client/modeller/figure-slot-layout-panel.js +25 -8
  43. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
  44. package/dist-client/modeller/figure-verdict.js +4 -1
  45. package/dist-client/modeller/figure-verdict.js.map +1 -1
  46. package/dist-client/modeller/unit-words.d.ts +5 -0
  47. package/dist-client/modeller/unit-words.js +12 -0
  48. package/dist-client/modeller/unit-words.js.map +1 -0
  49. package/dist-client/pages/figure-modeller-page.js +3 -2
  50. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  51. package/dist-client/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +9 -9
  53. package/test/browser/editing-page.js +3 -3
  54. package/test/browser/editing.check.mjs +5 -5
  55. package/test/browser/links-page.js +1 -1
  56. package/test/browser/links.check.mjs +2 -3
  57. package/test/browser/modeller-page.js +4 -4
  58. package/test/browser/modeller-revision.check.mjs +5 -5
  59. package/test/browser/modeller.check.mjs +5 -5
  60. package/test/browser/parts.check.mjs +2 -2
  61. package/test/browser/product-page.check.mjs +6 -0
  62. package/test/browser/props.check.mjs +3 -4
  63. package/test/browser/row-preview.check.mjs +3 -3
  64. package/test/browser/screen.check.mjs +13 -11
  65. package/test/consumer-capabilities.test.ts +7 -0
  66. package/test/links.test.ts +1 -1
  67. package/test/refusals.test.ts +1 -1
  68. package/translations/en.json +23 -12
  69. package/translations/es.json +23 -12
  70. package/translations/ja.json +24 -13
  71. package/translations/ko.json +82 -71
  72. package/translations/ms.json +23 -12
  73. package/translations/zh.json +23 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.225",
3
+ "version": "10.1.226",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,8 +27,8 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.12.0",
31
- "@hatiolab/things-scene": "^10.1.102",
30
+ "@hatiolab/figure-model": "^0.12.1",
31
+ "@hatiolab/things-scene": "^10.1.103",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.29.43",
34
34
  "@operato/graphql": "^10.29.43",
@@ -39,14 +39,14 @@
39
39
  "@operato/p13n": "^10.29.43",
40
40
  "@operato/popup": "^10.29.43",
41
41
  "@operato/shell": "^10.29.43",
42
- "@operato/slot-layout-editor": "^10.29.57",
42
+ "@operato/slot-layout-editor": "^10.29.58",
43
43
  "@operato/styles": "^10.0.0",
44
44
  "@operato/utils": "^10.29.43",
45
- "@things-factory/ai-assistant": "^10.1.223",
46
- "@things-factory/figure-service": "^10.1.224",
47
- "@things-factory/figure-viewer": "^10.1.224",
48
- "@things-factory/setting-base": "^10.1.223",
45
+ "@things-factory/ai-assistant": "^10.1.226",
46
+ "@things-factory/figure-service": "^10.1.226",
47
+ "@things-factory/figure-viewer": "^10.1.226",
48
+ "@things-factory/setting-base": "^10.1.226",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "9929ca76f614ff84bea1f61dabe0e1b7f00cd18a"
51
+ "gitHead": "abc5cf99effa2de579a1cbfda74ddc5eb41dba50"
52
52
  }
@@ -96,7 +96,7 @@ window.editing = {
96
96
  propertyInputs() {
97
97
  /* The panel shown: the properties, or the 「반응형」 tab a check pressed. */
98
98
  const panel = editor().renderRoot.querySelector('div[panel]:not([off])')
99
- return [...panel.querySelectorAll('input')].map(i => i.getAttribute('aria-label'))
99
+ return [...panel.querySelectorAll('input')].map(i => (i.dataset?.field ?? i.getAttribute('aria-label')))
100
100
  },
101
101
  /** The page point of the first element a selector finds -- anywhere on the page, or inside the `host` elements -- and what it says. */
102
102
  at(selector, host) {
@@ -126,13 +126,13 @@ window.editing = {
126
126
  },
127
127
  /** The page point of a field's label: the axis letter of a row's cell, or a single field's name. */
128
128
  fieldLabel(id) {
129
- const input = deep('input').find(i => i.getAttribute('aria-label') === id)
129
+ const input = deep('input').find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === id)
130
130
  const label = input?.closest('div[cell]')?.querySelector('span[axis]') ?? input?.previousElementSibling
131
131
  return label ? centreOf(label) : null
132
132
  },
133
133
  /** What a field shows now. */
134
134
  fieldValue(id) {
135
- return deep('input').find(i => i.getAttribute('aria-label') === id)?.value ?? null
135
+ return deep('input').find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === id)?.value ?? null
136
136
  },
137
137
  /** How a clip track's head is laid out: its name, the repeat choice and the remove button, as boxes on the page. */
138
138
  trackHead() {
@@ -314,11 +314,11 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
314
314
  })
315
315
 
316
316
  /*
317
- A round body's measures are the 「배치」 tab's size editor (2026-10-24): its across said as a diameter, written as the
317
+ A round body's measures are the 「크기」 tab's size editor (2026-10-24): its across said as a diameter, written as the
318
318
  radius it halves to. The editor writes on change; it draws nothing while a number is typed.
319
319
  */
320
320
  test('a diameter typed on the arrangement tab is written once when it is left, as the radius it halves to', async () => {
321
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
321
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
322
322
  const start = await revision()
323
323
  await type('drum X · figure.label.size-diameter figure.label.size-measure', '600')
324
324
  assert.equal(await revision(), start, 'nothing written yet')
@@ -332,7 +332,7 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
332
332
  })
333
333
 
334
334
  /*
335
- A box's size is the 「배치」 tab's size editor now (2026-10-24); the numbers in hand on the properties tab are its place,
335
+ A box's size is the 「크기」 tab's size editor now (2026-10-24); the numbers in hand on the properties tab are its place,
336
336
  in the position row.
337
337
  */
338
338
  describe('values in hand on the properties tab: drawn at once, written on change (user 2026-09-24)', () => {
@@ -374,7 +374,7 @@ describe('values in hand on the properties tab: drawn at once, written on change
374
374
  })
375
375
  })
376
376
 
377
- /* A size of the part's own on the 「배치」 tab's size editor is a value in hand as any field is (user 2026-09-24). */
377
+ /* A size of the part's own on the 「크기」 tab's size editor is a value in hand as any field is (user 2026-09-24). */
378
378
  describe('a size in hand on the arrangement tab: drawn while typed or dragged, written once', () => {
379
379
  const WIDTH = 'box X figure.label.size-measure'
380
380
  const wide = async () => Math.round((await look('box')).size[0])
@@ -382,7 +382,7 @@ describe('a size in hand on the arrangement tab: drawn while typed or dragged, w
382
382
  before(async () => {
383
383
  await mount('box')
384
384
  await pick('box')
385
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
385
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
386
386
  })
387
387
 
388
388
  test('a width typed is drawn while it is typed, and written once on Enter', async () => {
@@ -39,7 +39,7 @@ window.look = {
39
39
  all(selector, text) {
40
40
  return deep(selector)
41
41
  .filter(saying(text))
42
- .map(e => ({ text: words(e), disabled: !!e.disabled, open: !!e.open, value: e.value ?? null, label: e.getAttribute('aria-label'), readOnly: !!e.readOnly, spotlit: e.hasAttribute('spotlit') }))
42
+ .map(e => ({ text: words(e), disabled: !!e.disabled, open: !!e.open, value: e.value ?? null, label: e.dataset?.field ?? e.getAttribute('aria-label'), readOnly: !!e.readOnly, spotlit: e.hasAttribute('spotlit') }))
43
43
  },
44
44
  /** A face of the target box in the fastening drawing: whether it can be pressed now, and whether it is the one chosen. */
45
45
  face(f) {
@@ -88,9 +88,8 @@ async function pick(part) {
88
88
  await click(await read(id => probe.row(id), part), `the row of ${part}`)
89
89
  await until(id => probe.state().selected === id, [part], `${part} selected`)
90
90
  }
91
- /* A side tab by its short name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
92
- const tab = name =>
93
- read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name).then(point => click(point, `the ${name} tab`))
91
+ /* A side tab by its short name: one name whatever is picked (「크기」, sweep 2026-10-27 J2). */
92
+ const tab = name => read(n => probe.tab(`figure.label.tab-short-${n}`), name).then(point => click(point, `the ${name} tab`))
94
93
  /** A fastening the part has is said in one line; 「바꾸기」 opens the fastening window on it. */
95
94
  async function changeFastening() {
96
95
  await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
@@ -203,9 +203,9 @@ window.probe = {
203
203
  const l = Math.hypot(b.x - a.x, b.y - a.y) || 1
204
204
  return { x: (b.x - a.x) / l, y: (b.y - a.y) / l }
205
205
  },
206
- /** The page point of an input or checkbox by its aria-label. */
206
+ /** The page point of an input or checkbox by its field id (data-field) or its aria-label. */
207
207
  labelled(label) {
208
- const input = deep('input').find(i => i.getAttribute('aria-label') === label)
208
+ const input = deep('input').find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === label)
209
209
  return input ? centreOf(input) : null
210
210
  },
211
211
  /** The page point of a button by an attribute it carries. */
@@ -262,7 +262,7 @@ window.probe = {
262
262
  focused() {
263
263
  let at = document.activeElement
264
264
  while (at?.shadowRoot?.activeElement) at = at.shadowRoot.activeElement
265
- return at ? { tag: at.localName, label: at.getAttribute('aria-label') } : null
265
+ return at ? { tag: at.localName, label: at.dataset?.field ?? at.getAttribute('aria-label') } : null
266
266
  },
267
267
  /** The page point of an input by its aria-label, or of the part name field. */
268
268
  nameField() {
@@ -276,7 +276,7 @@ window.probe = {
276
276
  },
277
277
  /** Whether an input with this label is on the page. */
278
278
  hasInput(label) {
279
- return deep('input').some(i => i.getAttribute('aria-label') === label)
279
+ return deep('input').some(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === label)
280
280
  },
281
281
  /** The page point of a part's row in the parts list. */
282
282
  row(id) {
@@ -74,7 +74,7 @@ describe('work counted on the robot arm', () => {
74
74
 
75
75
  /*
76
76
  The tabs are drawn out of sight with the rest: the selected part's motion is evaluated at its two ends, and the size
77
- editor on 「배치」 asks once which of the parent's axes each of the part's own lies along, the figure at rest
77
+ editor on 「크기」 asks once which of the parent's axes each of the part's own lies along, the figure at rest
78
78
  (figureAxisPairsOf, ADR-0159 3) -- one evaluation for all three rows.
79
79
  */
80
80
  test('picking a part on the canvas compiles nothing; it evaluates the two ends of the part\'s motion and its axes at rest', async () => {
@@ -98,8 +98,8 @@ describe('work counted on the robot arm', () => {
98
98
  ends, and its axes paired at rest for the size editor once more on the new version.
99
99
  */
100
100
  test('a value typed into its field: the edit and its read, and nothing else while it is drawn', async () => {
101
- /* The part's measures are the 「배치」 tab's size editor (2026-10-24). */
102
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
101
+ /* The part's measures are the 「크기」 tab's size editor (2026-10-24). */
102
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
103
103
  await settled()
104
104
  const field = await read(() => {
105
105
  const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
@@ -189,8 +189,8 @@ describe('the edit canvas and the preview stay standing', () => {
189
189
  test('a value changed beside the preview is drawn inside the scene that stands', async () => {
190
190
  await click(await read(() => probe.row('box')))
191
191
  await sleep(300)
192
- /* The box's size is the 「배치」 tab's (2026-10-24). */
193
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
192
+ /* The box's size is the 「크기」 tab's (2026-10-24). */
193
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
194
194
  await sleep(300)
195
195
  const field = await read(() => probe.labelled('box Y figure.label.size-measure'))
196
196
  assert.ok(field, 'the height field')
@@ -54,10 +54,10 @@ async function click(point) {
54
54
  await sleep(400)
55
55
  }
56
56
  /*
57
- A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17), named
58
- 「배치」 (figure.label.tab-arrangement) while a part is chosen.
57
+ A side tab pressed, by its short name; how a part answers a size is the 「크기」 tab's (user 2026-10-17, one name
58
+ whatever is picked since the sweep of 2026-10-27, J2).
59
59
  */
60
- const tab = async name => click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name))
60
+ const tab = async name => click(await read(n => probe.tab(`figure.label.tab-short-${n}`), name))
61
61
 
62
62
  const KEYS = { translate: 'KeyW', rotate: 'KeyE', scale: 'KeyR' }
63
63
 
@@ -430,7 +430,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
430
430
  const field = id =>
431
431
  page().evaluateHandle(id => {
432
432
  const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
433
- return deep().find(i => i.getAttribute('aria-label') === id) ?? null
433
+ return deep().find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === id) ?? null
434
434
  }, id)
435
435
  const declared = () =>
436
436
  page().evaluate(() => {
@@ -474,7 +474,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
474
474
  const field = () =>
475
475
  page().evaluateHandle(() => {
476
476
  const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
477
- return deep().find(i => i.getAttribute('aria-label') === 'size.x.max') ?? null
477
+ return deep().find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === 'size.x.max') ?? null
478
478
  })
479
479
  /* Selected where it is drawn, as a person does, then typed over. */
480
480
  const typeOver = async keys => {
@@ -76,9 +76,9 @@ async function press(...keys) {
76
76
  for (const key of keys.slice(0, -1).reverse()) await page().keyboard.up(key)
77
77
  await sleep(400)
78
78
  }
79
- /* A side tab by its short name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
79
+ /* A side tab by its short name: one name whatever is picked (「크기」, sweep 2026-10-27 J2). */
80
80
  async function tab(name) {
81
- await click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name), `the ${name} tab`)
81
+ await click(await read(n => probe.tab(`figure.label.tab-short-${n}`), name), `the ${name} tab`)
82
82
  }
83
83
 
84
84
  describe('the parts list: each button does what it says, one step of the session each', () => {
@@ -189,6 +189,9 @@ describe('the first save of a new figure', () => {
189
189
  })
190
190
 
191
191
  test('the question asks the name first, focused, with the type written before', async () => {
192
+ /* A new figure saves once it has a part (sweep 2026-10-27 U24): the start's box. */
193
+ assert.equal(await read(() => product.deep('figure-editor >>> button[save]')[0].disabled), true, 'nothing to save before a part')
194
+ await press('figure-editor >>> button[start-shape="box"]', 'a box from the start')
192
195
  await press('button[save]', 'save')
193
196
  await until(() => product.deep('figure-first-save')[0]?.shadowRoot.querySelector('#type'), 'the first save asked')
194
197
  await until(() => { let a = document.activeElement; while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement; return a?.id === 'name' }, 'the name focused')
@@ -283,6 +286,8 @@ describe('opening another figure', () => {
283
286
  await read(() => product.deep('figure-editor')[0].updateComplete)
284
287
  assert.equal(await read(() => product.deep('figure-editor >>> section[empty-figure]').length), 0)
285
288
  assert.equal(await read(() => product.page().context.title), 'figure.label.figure')
289
+ /* Named by the address before it is read: the rail marks the catalogue, not "new figure" (sweep 2026-10-27 U30). */
290
+ assert.equal(await read(() => product.page().context.menu), 'figure-list')
286
291
  await read(() => window.letB())
287
292
  await until(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value === 'type-b', 'b\'s type')
288
293
  })
@@ -603,6 +608,7 @@ describe('the page around the figure: the shell, the dock, the properties', () =
603
608
  assert.deepEqual(await read(() => assistant.surface.anchor()), { anchorType: 'figure', anchorId: creationId })
604
609
  assert.deepEqual(await read(() => { const { figureId, editorMode } = assistant.surface.hostContext(); return { figureId, editorMode } }), { figureId: creationId, editorMode: 'new' })
605
610
  assert.equal((await read(() => product.context())).menu, undefined)
611
+ await press('figure-editor >>> button[start-shape="box"]', 'a box from the start')
606
612
  await rename('Gate')
607
613
  await read(() => { assistant.forget(); product.forget() })
608
614
  await press('button[save]', 'save')
@@ -94,10 +94,9 @@ const groupsInOrder = () =>
94
94
  .map(([name]) => name)
95
95
  })
96
96
 
97
- /* A side tab pressed, by its name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
97
+ /* A side tab pressed, by its name: one name whatever is picked (「크기」, sweep 2026-10-27 J2). */
98
98
  async function tab(name) {
99
- const named = `aside button[tab][aria-label="figure.label.tab-short-${name}"]`
100
- await press(name === 'response' ? `${named}, aside button[tab][aria-label="figure.label.tab-arrangement"]` : named, `the ${name} tab`)
99
+ await press(`aside button[tab][aria-label="figure.label.tab-short-${name}"]`, `the ${name} tab`)
101
100
  await read(() => document.querySelector('figure-editor').updateComplete)
102
101
  }
103
102
 
@@ -152,7 +151,7 @@ describe('the side: six tabs, the view and the releases from the bar', () => {
152
151
  describe('a part\'s groups in one order, whatever its state', () => {
153
152
  before(() => mount('rollers'))
154
153
 
155
- /* A box's size is the 「배치」 tab's (2026-10-24): it has no measures of its own shape left on the properties. */
154
+ /* A box's size is the 「크기」 tab's (2026-10-24): it has no measures of its own shape left on the properties. */
156
155
  test('a part fastened in a row: name, place, look, more; on 「반응형」 its size and its row', async () => {
157
156
  await pick('roller')
158
157
  await tab('properties')
@@ -60,10 +60,10 @@ describe('a row\'s preview follows its basis', () => {
60
60
  const at = await read(() => probe.row('bin'))
61
61
  await page().mouse.click(at.x, at.y)
62
62
  await until(() => probe.state().selected === 'bin', [], 'bin selected')
63
- /* The row is the 「반응형」 tab's (user 2026-10-17), named 「배치」 while a part is chosen (figure.label.tab-arrangement). */
64
- const tab = await read(() => probe.tab('figure.label.tab-arrangement'))
63
+ /* The row is the 「반응형」 tab's (user 2026-10-17), named 「크기」 (sweep 2026-10-27 J2). */
64
+ const tab = await read(() => probe.tab('figure.label.tab-short-response'))
65
65
  await page().mouse.click(tab.x, tab.y)
66
- await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-arrangement', [], 'the 배치 tab on')
66
+ await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-response', [], 'the 크기 tab on')
67
67
  await click('details[row-fold] > summary', {}, 'the row\'s fold')
68
68
  })
69
69
 
@@ -82,8 +82,8 @@ const tabOn = () => read(() => screenCheck.editor().shadowRoot.querySelector('bu
82
82
  const pressBar = action => press(action, a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`), action)
83
83
  const pressTab = name => press(name, name => screenCheck.editor().shadowRoot.querySelector(`button[tab][aria-label="${name}"]`), name)
84
84
  const TAB = name => `figure.label.tab-short-${name}`
85
- /* 「반응형」 while a part is chosen: named by what it holds for the part, its size and arrangement (2026-10-24). */
86
- const ARRANGEMENT = 'figure.label.tab-arrangement'
85
+ /* The size tab, one name whatever is picked (「크기」, sweep 2026-10-27 J2; it was 「배치」 while a part was chosen). */
86
+ const ARRANGEMENT = 'figure.label.tab-short-response'
87
87
  /*
88
88
  The view settings or the releases, opened from the bar over the work (they were tabs once); left open if open. The
89
89
  releases open from 「발행」, the bar's one way to them (user 2026-10-18).
@@ -220,7 +220,9 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
220
220
  test('the bar: undo and redo with their keys, the status, save, and the left pane folded to give the canvas its width', async () => {
221
221
  assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').pane === screenCheck.editor().shadowRoot.querySelector('ox-three-pane')), true, 'the bar folds the editor\'s own frame')
222
222
  const titles = await read(() => ['undo', 'redo'].map(a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`).title))
223
- assert.ok(titles[0].endsWith('(Ctrl+Z)') && titles[1].endsWith('(Ctrl+Y)'), `the keys in the titles: ${titles}`)
223
+ /* The keys as this platform writes them (sweep 2026-10-27 U26): ⌘Z · ⇧⌘Z on a Mac, Ctrl+Z · Ctrl+Y elsewhere. */
224
+ const mac = await read(() => navigator.userAgent.includes('Mac'))
225
+ assert.ok(titles[0].endsWith(mac ? '(⌘Z)' : '(Ctrl+Z)') && titles[1].endsWith(mac ? '(⇧⌘Z)' : '(Ctrl+Y)'), `the keys in the titles: ${titles}`)
224
226
  /* Saved or not is the save button's: pressable with something to save, dimmed without -- no words beside it. */
225
227
  const status = () => read(() => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar [role=status]')?.textContent.trim() ?? null)
226
228
  const saveOn = () => read(() => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]').disabled)
@@ -228,7 +230,7 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
228
230
  assert.equal(await saveOn(), false, 'saved: the save button is dimmed')
229
231
 
230
232
  await pressRow('box')
231
- /* A part's size is the 「배치」 tab's (2026-10-24). */
233
+ /* A part's size is the 「크기」 tab's (2026-10-24). */
232
234
  await pressTab(ARRANGEMENT)
233
235
  const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box Y figure.label.size-measure'))
234
236
  await height.click({ clickCount: 3 })
@@ -339,10 +341,10 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
339
341
  await pressTab(TAB('properties'))
340
342
  let now = await holds()
341
343
  assert.ok(now.subject, 'the part\'s head')
342
- /* A box's size is the 「배치」 tab's, in its size editor (2026-10-24): the properties keep its place. */
344
+ /* A box's size is the 「크기」 tab's, in its size editor (2026-10-24): the properties keep its place. */
343
345
  assert.ok(now.links.includes('figure.label.group-place') && !now.links.includes('figure.label.group-size'), `the shape's links: ${now.links}`)
344
346
  assert.ok(now.material, 'its material')
345
- /* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」), named 「배치」 while a part is chosen. */
347
+ /* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」), named 「크기」. */
346
348
  await pressTab(ARRANGEMENT)
347
349
  now = await holds()
348
350
  assert.ok(now.subject, 'the part\'s head')
@@ -732,7 +734,7 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
732
734
  test('a width typed ten times larger fits the view again; a little wider leaves the camera where it is', async () => {
733
735
  await mount('box')
734
736
  await pressRow('box')
735
- /* A part's size is the 「배치」 tab's (2026-10-24). */
737
+ /* A part's size is the 「크기」 tab's (2026-10-24). */
736
738
  await pressTab(ARRANGEMENT)
737
739
  await read(() => {
738
740
  const canvas = screenCheck.deep('figure-canvas')[0]
@@ -806,7 +808,7 @@ describe('a part\'s size row: a rule\'s numbers only where a rule sets every sid
806
808
  })
807
809
 
808
810
  /*
809
- A box's size is the 「배치」 tab's size editor (2026-10-24), one row a side: made to follow on every side, each row
811
+ A box's size is the 「크기」 tab's size editor (2026-10-24), one row a side: made to follow on every side, each row
810
812
  shows the size the rule gives it, and the properties draw no size row.
811
813
  */
812
814
  test('a box made to follow the figure\'s size on every side shows the size the rule gives it', async () => {
@@ -848,7 +850,7 @@ describe('the side panel is found as it was left, per part', () => {
848
850
  before(() => mount('arm'))
849
851
 
850
852
  /*
851
- The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one gives the panel
853
+ The part's properties fit a tall window since its size moved to 「크기」 (2026-10-24): a shorter one gives the panel
852
854
  something to scroll.
853
855
  */
854
856
  test('scrolled and a fold opened on one part; another part opens at its top; back, the first is as it was left', async () => {
@@ -1106,7 +1108,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1106
1108
 
1107
1109
  test('an edit is judged as it stands on screen: the source the server is sent is the one edited', async () => {
1108
1110
  await pressRow('left')
1109
- /* A part's size is the 「배치」 tab's (2026-10-24). */
1111
+ /* A part's size is the 「크기」 tab's (2026-10-24). */
1110
1112
  await pressTab(ARRANGEMENT)
1111
1113
  const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'left Y figure.label.size-measure'))
1112
1114
  await height.click({ clickCount: 3 })
@@ -1148,7 +1150,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1148
1150
  }
1149
1151
  })
1150
1152
 
1151
- /* The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one scrolls. */
1153
+ /* The part's properties fit a tall window since its size moved to 「크기」 (2026-10-24): a shorter one scrolls. */
1152
1154
  test('another figure opened starts its panels clean: a part of the same name opens at its top', async () => {
1153
1155
  const viewport = page().viewport()
1154
1156
  await page().setViewport({ ...viewport, height: 600 })
@@ -14,6 +14,7 @@ import {
14
14
  inputTableAt,
15
15
  namesWith,
16
16
  needsMissing,
17
+ needsOffered,
17
18
  partsAt,
18
19
  settingsIssues,
19
20
  settingsOf,
@@ -58,6 +59,12 @@ test('what a capability still needs: the item mark, a name; put on and off keeps
58
59
  assert.deepEqual(needsMissing(CARRIER, asset), ['item'])
59
60
  assert.deepEqual(needsMissing(CARRIER, { ...asset, item: true }), [])
60
61
  assert.deepEqual(needsMissing({ ...(CARRIER as object), needs: ['Holdable', ['Capacity', 'FlowNode']] } as never, asset), ['Holdable', 'Capacity | FlowNode'])
62
+ /* Offered as the tab offers it (sweep 2026-10-27 U19): the item tag, and the rungs that give what is needed -- never its names. */
63
+ assert.deepEqual(needsOffered(CARRIER, asset), { item: true, rungs: [], names: [] })
64
+ const holder = { ...(CARRIER as object), needs: [['Holdable', 'FlowNode'], ['Capacity', 'FlowNode']] } as never
65
+ assert.deepEqual(needsOffered(holder, asset), { item: false, rungs: ['hold', 'flow'], names: [] })
66
+ assert.deepEqual(needsOffered({ ...(CARRIER as object), needs: ['FlowNode'] } as never, asset), { item: false, rungs: ['flow'], names: [] })
67
+ assert.deepEqual(needsOffered({ ...(CARRIER as object), needs: ['Nowhere'] } as never, asset), { item: false, rungs: [], names: ['Nowhere'] })
61
68
  const held = editFigurePart(asset, { kind: 'set-capabilities', names: ['Capacity', 'Holdable'] })
62
69
  assert.deepEqual(namesWith(held, 'acme.Carrier', true), ['Capacity', 'Holdable', 'acme.Carrier'])
63
70
  assert.deepEqual(namesWith(editFigurePart(held, { kind: 'set-capabilities', names: namesWith(held, 'acme.Carrier', true) }), 'acme.Carrier', false), ['Capacity', 'Holdable'])
@@ -630,7 +630,7 @@ test('팔레트의 통은 통의 크기로 서고, 세 축 모두 크기를 따
630
630
  assert.deepEqual(followableAxes(asset, 'tote'), ['x', 'y', 'z'])
631
631
  assert.deepEqual(gizmoReachOf(asset as never, 'tote').sizes, ['tote.dimension.width', 'tote.dimension.height', 'tote.dimension.depth'])
632
632
  const ko = wordsOf('ko')
633
- assert.equal(ko['figure.label.face-inside-bottom'], '안쪽 바닥')
633
+ assert.equal(ko['figure.label.face-inside-bottom'], '안쪽 바닥면')
634
634
  })
635
635
 
636
636
  /*
@@ -205,7 +205,7 @@ test('일상에서 코드로만 보이던 거절 넷 — 줄이 안 들어감 ·
205
205
  layout: { tiers: [{ tierNo: 1, anchorId: 'bin', mount: 'ON', position: { x: 0, y: 0, z: 0 }, rotation: 0, layout: { pattern: 'SINGLE', slot: { allowed: { width: 100, depth: 100, height: 100 }, accepts: ['TOTE'] } } }] }
206
206
  })
207
207
  const held = editRefusalOf(refusalOf(() => act(slotted, { kind: 'make-repeat', part: 'bin', axis: 'x', pitch: 150 })), say)
208
- assert.ok(held.includes('bin') && held.includes('적재 위치 배치') && !held.includes('SURFACE_PROVENANCE'), held)
208
+ assert.ok(held.includes('bin') && held.includes('자리 배치') && !held.includes('SURFACE_PROVENANCE'), held)
209
209
  /* A new part named as the row's part is (its copies drawn under ids of their own): the same words as a rename. */
210
210
  assert.match(editRefusalOf(refusalOf(() => fm.addFigureBox(conveyor(), { id: 'roller', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } })), keyed), /part-name-taken .*"name":"roller"/)
211
211
  })
@@ -19,8 +19,8 @@
19
19
  "figure.label.group-more": "More",
20
20
  "figure.label.group-place": "Place",
21
21
  "figure.label.group-position-response": "Position",
22
- "figure.label.tab-response": "Responsive -- when the size changes",
23
- "figure.label.tab-short-response": "Responsive",
22
+ "figure.label.tab-response": "Size · when the size changes",
23
+ "figure.label.tab-short-response": "Size",
24
24
  "figure.text.sizing-uniform-proposed": "this figure has joints. growing it by one ratio keeps the joints together at any size.",
25
25
  "figure.text.sizing-uniform-explained": "grows the whole figure by the geometric mean of the three sizes a board gives it. the feet stay on the floor; room left above the figure stays empty.",
26
26
  "figure.button.mirror-x": "across X",
@@ -77,7 +77,7 @@
77
77
  "figure.text.gizmo-joint-origin-follows": "the {joint} joint's place follows another part. change it under place in the properties tab.",
78
78
  "figure.text.gizmo-joint-off-axis": "the {joint} joint turns only about {axis}. use the {axis} rotate handle.",
79
79
  "figure.text.gizmo-joint-several-axes": "the {joint} joint moves on several axes, so it cannot be dragged. adjust its values on the motion tab.",
80
- "figure.text.gizmo-size-follows": "{part}'s {axis} size follows another measure. change it under size in the responsive tab.",
80
+ "figure.text.gizmo-size-follows": "{part}'s {axis} size follows another measure. change it in the size tab.",
81
81
  "figure.text.gizmo-fastening-places": "{part} is fastened to a face of another part, which places it. detach it first to move it.",
82
82
  "figure.text.size-range-leaves-out-size": "the {axis} range on a drawing, {range} mm, does not hold its size now, {at} mm.",
83
83
  "figure.text.size-range-from-zero": "the smallest {axis} on a drawing must be above 0.",
@@ -360,6 +360,15 @@
360
360
  "figure.label.triangles": "triangles",
361
361
  "figure.label.unit-none": "no unit",
362
362
  "figure.label.unit-type-it": "type it…",
363
+ "figure.label.unit-mm": "mm",
364
+ "figure.label.unit-cm": "cm",
365
+ "figure.label.unit-m": "m",
366
+ "figure.label.unit-rad": "radians",
367
+ "figure.label.unit-deg": "degrees",
368
+ "figure.label.unit-s": "seconds",
369
+ "figure.label.unit-ratio": "ratio",
370
+ "figure.label.unit-percent": "%",
371
+ "figure.label.unit-count": "count",
363
372
  "figure.label.weight": "drawing cost",
364
373
  "figure.text.accepts-anything": "empty means anything",
365
374
  "figure.text.accepts-explained": "types allowed in this slot. separate with commas; leave empty for no limit.",
@@ -381,6 +390,8 @@
381
390
  "figure.text.trend-flat-lines-overlap": "the flat lines overlap at {value}.",
382
391
  "figure.text.cannot-release-now": "cannot be released yet",
383
392
  "figure.text.nothing-blocks-release": "nothing in the figure as it stands blocks releasing it.",
393
+ "figure.text.release-needs-a-part": "Judged once a part is placed.",
394
+ "figure.text.save-needs-a-part": "Place a part to save.",
384
395
  "figure.text.verdict-asking": "checking…",
385
396
  "figure.text.verdict-not-received": "the check did not come back — {reason}",
386
397
  "figure.text.verdict-waiting": "the check follows your edits.",
@@ -848,16 +859,17 @@
848
859
  "figure.label.tab-short-capability": "Capability",
849
860
  "figure.label.tab-short-motion": "Motion",
850
861
  "figure.label.tab-short-verdict": "Check",
862
+ "figure.label.tab-verdict": "Check · maturity and the release verdict",
851
863
  "figure.label.tab-short-view": "View",
852
864
  "figure.label.tab-short-history": "History",
853
865
  "figure.text.start-question": "What would you like to start with?",
854
866
  "figure.label.start-shape": "Add a basic shape",
855
867
  "figure.text.start-shape-more": "More shapes are in the list on the left.",
868
+ "figure.button.start-shape-more": "More parts",
856
869
  "figure.label.start-recipe": "Start from a recipe",
857
870
  "figure.button.start-recipe": "Browse recipes",
858
871
  "figure.label.start-ai": "Draft with AI",
859
872
  "figure.button.start-ai": "Ask AI for a draft",
860
- "figure.text.start-ai-optional": "Optional. You can build it yourself.",
861
873
  "figure.text.empty-figure-next": "Add a part to see its size and placement.",
862
874
  "figure.label.figure-info-later": "Figure information · you can set this later",
863
875
  "figure.text.parts-panel-folded-narrow": "The screen is narrow, so the parts list is folded. The panel button in the toolbar opens it again.",
@@ -1204,7 +1216,6 @@
1204
1216
  "figure.text.size-unpaired": "it lies at an angle, so the axis to follow cannot be told. pick a source and an axis.",
1205
1217
  "figure.label.repeat-inactive": "Not applied",
1206
1218
  "figure.label.repeat-active": "Applied",
1207
- "figure.label.tab-arrangement": "Layout",
1208
1219
  "figure.text.repeat-intro": "Repeat this part along the length of the part it is attached to.",
1209
1220
  "figure.text.repeat-direction": "Direction uses the attached parent’s axes.",
1210
1221
  "figure.text.repeat-draft": "Preview only. Apply to confirm this layout.",
@@ -1216,11 +1227,8 @@
1216
1227
  "figure.button.state-size-fix": "Back to fixed",
1217
1228
  "figure.label.state-size-measure": "Measure",
1218
1229
  "figure.label.state-size-unit": "Unit",
1219
- "figure.label.state-unit-percent": "%",
1220
- "figure.label.state-unit-ratio": "ratio",
1221
- "figure.label.state-unit-mm": "mm",
1222
- "figure.label.state-size-at-min": "At its least",
1223
- "figure.label.state-size-at-max": "At its most",
1230
+ "figure.label.state-size-at-min": "at {at}",
1231
+ "figure.label.state-size-at-max": "at {at}",
1224
1232
  "figure.button.add-state-size": "Add size by state",
1225
1233
  "figure.text.summary-size-state": "Its {axes} changes with {control}.",
1226
1234
  "figure.label.follows-state": "by {control}",
@@ -1279,9 +1287,12 @@
1279
1287
  "figure.text.data-table-target-part": "Part {id} is not there, or is chosen twice.",
1280
1288
  "figure.label.product-capabilities": "Product capabilities",
1281
1289
  "figure.text.product-capability-needs": "Needs first: {needs}",
1290
+ "figure.text.product-capability-needs-rungs": "the {rungs} function",
1291
+ "figure.label.or-between": " or ",
1282
1292
  "figure.text.product-capability-unknown": "The figure wears {name}, a product capability this modeller does not know.",
1283
1293
  "figure.button.product-capability-remove": "Remove",
1284
1294
  "figure.text.data-capability-fields": "Data {name} reads itself: {fields}.",
1285
- "figure.text.data-binding-rests": "In {where} the {name} capability takes this over — this binding rests",
1286
- "figure.text.data-binding-rests-targets": "In {where} the {name} capability takes over: {targets} — those targets rest"
1295
+ "figure.label.data-binding-resting": "resting",
1296
+ "figure.text.data-binding-rests": "In {where} the {name} capability does this instead.",
1297
+ "figure.text.data-binding-rests-targets": "In {where} the {name} capability does this instead for: {targets}"
1287
1298
  }