@things-factory/figure-ui 10.1.176 → 10.1.177

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.176",
3
+ "version": "10.1.177",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -41,11 +41,11 @@
41
41
  "@operato/slot-layout-editor": "^10.29.25",
42
42
  "@operato/styles": "^10.0.0",
43
43
  "@operato/utils": "^10.0.0",
44
- "@things-factory/ai-assistant": "^10.1.176",
45
- "@things-factory/figure-service": "^10.1.176",
44
+ "@things-factory/ai-assistant": "^10.1.177",
45
+ "@things-factory/figure-service": "^10.1.177",
46
46
  "@things-factory/figure-viewer": "^10.1.176",
47
47
  "@things-factory/setting-base": "^10.1.176",
48
48
  "three": "^0.185.1"
49
49
  },
50
- "gitHead": "24928f127d73de0e9946423bd4c3295f54cce03d"
50
+ "gitHead": "91da09c945ec5e77ab960074db8f53b9ea8ac534"
51
51
  }
@@ -94,7 +94,8 @@ window.editing = {
94
94
  },
95
95
  /** The labels of the number fields on the properties tab. */
96
96
  propertyInputs() {
97
- const panel = editor().renderRoot.querySelector('div[panel]')
97
+ /* The panel shown: the properties, or the 「반응형」 tab a check pressed. */
98
+ const panel = editor().renderRoot.querySelector('div[panel]:not([off])')
98
99
  return [...panel.querySelectorAll('input')].map(i => i.getAttribute('aria-label'))
99
100
  },
100
101
  /** The page point of the first element a selector finds -- anywhere on the page, or inside the `host` elements -- and what it says. */
@@ -90,11 +90,6 @@ async function pick(part) {
90
90
  await until(id => probe.state().selected === id, [part], `${part} selected`)
91
91
  }
92
92
  const tab = name => read(l => probe.tab(l), `figure.label.tab-short-${name}`).then(point => click(point, `the ${name} tab`))
93
- /** What the figure does is a folded group of the properties (chief architect 2026-10-16): its fold opened, as a person opens it. */
94
- async function openCapability() {
95
- const summary = await handle('section[capability] > details > summary')
96
- if (!(await summary.evaluate(s => s.parentElement.open))) await summary.click()
97
- }
98
93
  /** A fastening the part has is said in one line; 「바꾸기」 opens its drawing and details. */
99
94
  async function changeFastening() {
100
95
  await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
@@ -211,6 +206,8 @@ describe('a part made to follow the figure size, and let go of it', () => {
211
206
  a = applyFigureAuthoring(a, { kind: 'declare-instance-size', range: SIZE, at: { x: 400, y: 400, z: 400 } })
212
207
  await mount('sized', applyFigureAuthoring(a, { kind: 'link-dimension', part: 'box', axis: 'x', source: { of: 'instance', axis: 'x' } }))
213
208
  await pick('box')
209
+ /* How a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
210
+ await tab('response')
214
211
  })
215
212
 
216
213
  test('on a figure with a size, follow-size is one step, says nothing of a size declared, and is not offered again', async () => {
@@ -253,6 +250,8 @@ function sizedPair(turning) {
253
250
  describe('the sizes a figure may be placed at, and how it grows', () => {
254
251
  test('measured again, the declared range becomes the measured one in one step, and the offer goes', async () => {
255
252
  await mount('sized-pair', sizedPair(false))
253
+ /* The figure's range is the 「반응형」 tab's. */
254
+ await tab('response')
256
255
  const start = await state()
257
256
  await click(await at('button', 'figure.button.remeasure-range'), 'remeasure')
258
257
  await oneStep(start)
@@ -270,6 +269,7 @@ describe('the sizes a figure may be placed at, and how it grows', () => {
270
269
 
271
270
  test('a figure that turns about a joint is urged to grow by one ratio', async () => {
272
271
  await mount('turning', sizedPair(true))
272
+ await tab('response')
273
273
  assert.equal((await all('p[hint]', 'figure.text.sizing-uniform-proposed')).length, 1)
274
274
  })
275
275
  })
@@ -285,6 +285,8 @@ describe('a row\'s pitch', () => {
285
285
  test('a pitch that follows the frame\'s length shows as the number it comes to, and the row is said as drawn', async () => {
286
286
  await mount('pitch-follows', applyFigureAuthoring(conveyor(), { kind: 'set-repeat', part: 'roller', pitch: { from: { of: 'part', part: 'frame', axis: 'x' }, times: 0.1 } }))
287
287
  await pick('roller')
288
+ /* The row is the 「반응형」 tab's. */
289
+ await tab('response')
288
290
  assert.deepEqual((await all('input[aria-label="roller.repeat.pitch"]')).map(i => i.value), ['200'], 'a tenth of 2000')
289
291
  assert.ok((await all('ul[row-reading] li')).length > 0, 'the row said as it stands')
290
292
  })
@@ -297,6 +299,7 @@ describe('a row\'s pitch', () => {
297
299
  ])
298
300
  await mount('published-pitch', published)
299
301
  await pick('roller')
302
+ await tab('response')
300
303
  const start = await state()
301
304
  await type(await handle('input[aria-label="roller.repeat.pitch"]'), '300')
302
305
  await oneStep(start)
@@ -310,7 +313,8 @@ describe('holding places on a part', () => {
310
313
  test('a place needs kinds it takes; a figure that holds nothing says so, and declaring it holds things is one step', async () => {
311
314
  await mount('box')
312
315
  await pick('box')
313
- await openCapability()
316
+ /* What the figure does is a tab again (user 2026-10-17). */
317
+ await tab('capability')
314
318
  assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, true, 'no kinds: no place')
315
319
  await type(await handle('input[placeholder="BOX, TOTE"]'), 'BOX')
316
320
  assert.equal((await at('button[act]', 'figure.button.add-place'))?.disabled, false, 'a kind: a place may be made')
@@ -333,6 +337,8 @@ describe('a position worked out from the figure size', () => {
333
337
  a.document.model.nodes.find(n => n.id === 'lamp.pose').args[1] = 'lamp.lift.value'
334
338
  await mount('lifted', a)
335
339
  await pick('lamp')
340
+ /* A position that follows the figure size is the 「반응형」 tab's. */
341
+ await tab('response')
336
342
  assert.equal((await all('div[pair] span', 'figure.label.position-worked-out')).length, 1)
337
343
  const start = await state()
338
344
  await click(await at('button[act]', 'figure.button.scale-position'), 'scale position')
@@ -531,13 +537,15 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
531
537
 
532
538
  test('a part fastened to nothing: the row is folded under its head and says why', async () => {
533
539
  await pick('loose')
540
+ await tab('response')
534
541
  assert.deepEqual((await all('details[row-fold] > summary h3')).map(h => h.text), ['figure.label.repeat'])
535
542
  assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [false], 'folded')
536
543
  assert.deepEqual((await all('p[row-blocked]')).map(p => p.text), ['figure.text.row-blocked-unfastened'])
537
544
  })
538
545
 
539
- test('the row worked on is the one seen: its copies drawn and said; back on the fastening, it folds', async () => {
546
+ test('the row worked on is the one seen: its copies drawn and said; on the properties tab they are not', async () => {
540
547
  await pick('lamp')
548
+ await tab('response')
541
549
  assert.deepEqual((await all('details[row-fold]')).map(d => d.open), [false], 'folded')
542
550
  await read(() => look.hear('preview-command'))
543
551
  /* One click opens it, and it stays open while the row is worked. */
@@ -549,8 +557,9 @@ describe('a bench: what each part is fastened to, what detaching would do, a row
549
557
  assert.equal((await heard('preview-command')).filter(Boolean).at(-1).action.kind, 'make-repeat')
550
558
  await until(() => look.drawn().filter(n => n.startsWith('__preview')).length > 1, [], 'the copies drawn')
551
559
  await until(() => look.all('section[row] [preview-words]', 'figure.text.row-count-pitch').length === 1, [], 'the row said')
552
- await click(await at('section[fastening] h3 button'), 'the fastening section')
553
- await until(() => look.all('details[row-fold]').every(d => !d.open), [], 'the row folded again')
560
+ /* The properties tab shown: what its fastening would do is said, the row's copies are not drawn. */
561
+ await tab('properties')
562
+ await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'the copies not drawn on the properties tab')
554
563
  })
555
564
  })
556
565
 
@@ -53,6 +53,8 @@ async function click(point) {
53
53
  await page().mouse.click(point.x, point.y)
54
54
  await sleep(400)
55
55
  }
56
+ /* A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17). */
57
+ const tab = async name => click(await read(l => probe.tab(l), `figure.label.tab-short-${name}`))
56
58
 
57
59
  const KEYS = { translate: 'KeyW', rotate: 'KeyE', scale: 'KeyR' }
58
60
 
@@ -383,6 +385,7 @@ describe('the fastening form belongs to the part it is for', () => {
383
385
  describe('a figure made in the modeller is given its size from its parts (user 2026-09-25)', () => {
384
386
  before(async () => {
385
387
  await mount('box')
388
+ await tab('response')
386
389
  })
387
390
 
388
391
  test('with nothing picked, the figure offers to declare its size; clicked, its size fields appear', async () => {
@@ -697,6 +700,7 @@ describe('a part follows the figure size in one step (follow-size, 2026-09-26)',
697
700
  test('one click on a figure with no size: its size is declared from the part, said in a line, and the part follows it', async () => {
698
701
  await mount('box')
699
702
  await click(await read(() => probe.row('box')))
703
+ await tab('response')
700
704
  const at = await page().evaluate(() => {
701
705
  const deep = (root = document) => [...root.querySelectorAll('button[follow-size]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
702
706
  const b = deep()[0]
@@ -827,6 +831,7 @@ describe('a row\'s pitch made a board\'s to set (ADR-0096)', () => {
827
831
  test('published in one step with a name and a range; the pitch then sets the input; withdrawn, the pitch stays as it is', async () => {
828
832
  await mount('rollers')
829
833
  await click(await read(() => probe.row('roller')))
834
+ await tab('response')
830
835
  const before = (await read(() => probe.ids())).filter(id => id.includes('"roller.placed"')).length
831
836
  await click(await read(() => probe.labelled('figure.label.board-sets')))
832
837
  await typeInto('figure.label.board-name', '롤러 간격')
@@ -893,6 +898,7 @@ describe('the row section opens on one click', () => {
893
898
  test('a part that can be made a row: one click on the folded heading opens it, a second folds it', async () => {
894
899
  await mount('rollers')
895
900
  await click(await read(() => probe.row('mark')))
901
+ await tab('response')
896
902
  const summary = await page().evaluateHandle(() => {
897
903
  const deep = (root = document) => [...root.querySelectorAll('details[row-fold] > summary')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
898
904
  return deep()[0] ?? null
@@ -320,7 +320,8 @@ describe('a row is one part: selected whole, its pitch one value set by the row
320
320
 
321
321
  test('the pitch is offered once, and typed it is one step that sets the row again', async () => {
322
322
  await pick('roller')
323
- await tab('properties')
323
+ /* The row's pitch is the 「반응형」 tab's (user 2026-10-17). */
324
+ await tab('response')
324
325
  const fields = await read(() => editing.propertyInputs())
325
326
  assert.equal(fields.filter(label => /pitch|repeat/i.test(label ?? '')).length, 1, fields.join())
326
327
  const label = fields.find(label => /pitch|repeat/i.test(label ?? ''))
@@ -344,18 +345,8 @@ describe('the long explanations open from their 「?」 beside the title, closed
344
345
  await mount('box', 'doors')
345
346
  await pick('left')
346
347
  const seen = new Set()
347
- for (const name of ['properties', 'motion']) {
348
+ for (const name of ['properties', 'response', 'motion', 'capability']) {
348
349
  await tab(name)
349
- /* What the figure does is a folded group of the properties now (chief architect 2026-10-16): opened as a hand opens it. */
350
- if (name === 'properties') {
351
- const fold = await read(() => {
352
- const summary = document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details > summary')
353
- summary.scrollIntoView({ block: 'center' })
354
- const r = summary.getBoundingClientRect()
355
- return { x: r.left + 20, y: r.top + r.height / 2 }
356
- })
357
- await click(fold, 'the capability fold')
358
- }
359
350
  const helps = await read(() =>
360
351
  [...document.querySelector('figure-editor').renderRoot.querySelectorAll('figure-links, figure-clips')].flatMap((element, host) =>
361
352
  [...element.shadowRoot.querySelectorAll('button[help]')].map((b, i) => ({ host, i, seen: b.getClientRects().length > 0 })).filter(b => b.seen)
@@ -198,15 +198,10 @@ describe('places made by a rule: the shared slot layout editor on a part\'s face
198
198
  before(async () => {
199
199
  await mount(addFigureBox(figure('Stacker'), { id: 'fork', size: { x: 1200, y: 50, z: 900 }, position: { x: 0, y: 25, z: 0 } }))
200
200
  await pick('fork')
201
- /* What the figure does is a folded group of the properties (chief architect 2026-10-16): opened as a hand opens it. */
202
- const fold = await read(() => {
203
- const summary = document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details > summary')
204
- summary.scrollIntoView({ block: 'center' })
205
- const r = summary.getBoundingClientRect()
206
- return { x: r.left + 20, y: r.top + r.height / 2 }
207
- })
208
- await page().mouse.click(fold.x, fold.y)
209
- await until(() => !!document.querySelector('figure-editor').renderRoot.querySelector('div[panel]:not([off]) section[capability] > details[open]'), [], 'the capability fold open')
201
+ /* What the figure does is a tab of its own (user 2026-10-17). */
202
+ const tab = await read(l => probe.tab(l), 'figure.label.tab-short-capability')
203
+ await page().mouse.click(tab.x, tab.y)
204
+ await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-capability', [], 'the capability tab on')
210
205
  await settled()
211
206
  })
212
207
 
@@ -70,8 +70,8 @@ async function press(selector, what) {
70
70
  }
71
71
 
72
72
  /**
73
- * The groups of the panel shown, top to bottom, by what each is: the part's name, the size, the place and the row the
74
- * links draw, the look, what the figure does, and 「더 보기」.
73
+ * The groups of the panel shown, top to bottom, by what each is: the part's name, the size and the place the links
74
+ * draw, the look and 「더 보기」 on the properties tab; on 「반응형」 the size rules, a position that follows, the row.
75
75
  */
76
76
  const groupsInOrder = () =>
77
77
  read(() => {
@@ -80,11 +80,11 @@ const groupsInOrder = () =>
80
80
  const links = panel.querySelector('figure-links')?.shadowRoot
81
81
  const marks = {
82
82
  name: panel.querySelector('input[name-field]'),
83
- size: links?.querySelector('section[size]'),
83
+ size: links?.querySelector('section[size], section[size-response]'),
84
84
  place: links?.querySelector('section[fastening]'),
85
+ position: links?.querySelector('section[position-response]'),
85
86
  row: panel.querySelector('section[slot=row]') ?? links?.querySelector('section[row]'),
86
87
  look: panel.querySelector('figure-material-panel:not([show])'),
87
- capability: panel.querySelector('section[capability]'),
88
88
  more: panel.querySelector('section[more]')
89
89
  }
90
90
  return Object.entries(marks)
@@ -93,12 +93,18 @@ const groupsInOrder = () =>
93
93
  .map(([name]) => name)
94
94
  })
95
95
 
96
- describe('the side: four tabs, the view and the history from the bar', () => {
96
+ /* A side tab pressed, by its name. */
97
+ async function tab(name) {
98
+ await press(`aside button[tab][aria-label="figure.label.tab-short-${name}"]`, `the ${name} tab`)
99
+ await read(() => document.querySelector('figure-editor').updateComplete)
100
+ }
101
+
102
+ describe('the side: six tabs, the view and the history from the bar', () => {
97
103
  before(() => mount('rollers'))
98
104
 
99
- test('properties, motion, data and the verdict -- the four tabs, in that order', async () => {
105
+ test('properties, responsive, motion, capability, data and the verdict -- the six tabs, in that order (user 2026-10-17)', async () => {
100
106
  const tabs = await read(() => [...document.querySelector('figure-editor').shadowRoot.querySelectorAll('aside button[tab]')].map(b => b.getAttribute('aria-label')))
101
- assert.deepEqual(tabs, ['properties', 'motion', 'data', 'verdict'].map(t => `figure.label.tab-short-${t}`))
107
+ assert.deepEqual(tabs, ['properties', 'response', 'motion', 'capability', 'data', 'verdict'].map(t => `figure.label.tab-short-${t}`))
102
108
  })
103
109
 
104
110
  test('the view opens over the work from the bar, pressed while open; Esc closes it', async () => {
@@ -126,18 +132,27 @@ describe('the side: four tabs, the view and the history from the bar', () => {
126
132
  describe('a part\'s groups in one order, whatever its state', () => {
127
133
  before(() => mount('rollers'))
128
134
 
129
- test('a part fastened in a row: name, size, place, row, look, what it does, more', async () => {
135
+ test('a part fastened in a row: name, size, place, look, more; on 「반응형」 its size rules and its row', async () => {
130
136
  await pick('roller')
131
- assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'row', 'look', 'capability', 'more'])
137
+ await tab('properties')
138
+ assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
139
+ await tab('response')
140
+ assert.deepEqual(await groupsInOrder(), ['size', 'row'])
141
+ await tab('properties')
132
142
  })
133
143
 
134
144
  test('a part on the plane, not in a row: the same order, the row folded where a row would be', async () => {
135
145
  await pick('bed')
136
- assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'row', 'look', 'capability', 'more'])
146
+ assert.deepEqual(await groupsInOrder(), ['name', 'size', 'place', 'look', 'more'])
147
+ /* On the plane, its height is worked out from the figure size: the position group stands between. */
148
+ await tab('response')
149
+ assert.deepEqual(await groupsInOrder(), ['size', 'position', 'row'])
150
+ await tab('properties')
137
151
  })
138
152
 
139
153
  test('a fastened part\'s place is one line and 「바꾸기」: no locked cells, no drawing, until it is changed', async () => {
140
154
  await pick('mark')
155
+ await tab('properties')
141
156
  const place = () =>
142
157
  read(() => {
143
158
  const editor = document.querySelector('figure-editor')
@@ -187,6 +202,8 @@ describe('a group\'s 「⋯」: its rarer actions, by the mouse or the keyboard'
187
202
 
188
203
  test('Esc closes the menu and nothing is done', async () => {
189
204
  await pick('roller')
205
+ /* The row is the 「반응형」 tab's. */
206
+ await tab('response')
190
207
  const start = await revision()
191
208
  await press('section[row] button[group-menu]', 'the row\'s 「⋯」')
192
209
  await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
@@ -60,6 +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). */
64
+ const tab = await read(() => probe.tab('figure.label.tab-short-response'))
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-short-response', [], 'the 반응형 tab on')
63
67
  await click('details[row-fold] > summary', {}, 'the row\'s fold')
64
68
  })
65
69
 
@@ -191,13 +191,13 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
191
191
  assert.equal(await read(() => getComputedStyle(screenCheck.editor()).containerType), 'inline-size')
192
192
  })
193
193
 
194
- test('the side\'s four tabs, in order, each saying its name under its icon -- the name it is read by', async () => {
194
+ test('the side\'s six tabs, in order, each saying its name under its icon -- the name it is read by', async () => {
195
195
  const tabs = await read(() =>
196
196
  [...screenCheck.editor().shadowRoot.querySelectorAll('aside button[tab]')].map(b => ({ label: b.getAttribute('aria-label'), shows: b.querySelector('span[tab-label]')?.textContent.trim() }))
197
197
  )
198
198
  assert.deepEqual(
199
199
  tabs.map(t => t.label),
200
- ['properties', 'motion', 'data', 'verdict'].map(TAB)
200
+ ['properties', 'response', 'motion', 'capability', 'data', 'verdict'].map(TAB)
201
201
  )
202
202
  for (const tab of tabs) assert.equal(tab.shows, tab.label, `${tab.label} shows its name`)
203
203
  assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelector('aside').firstElementChild.matches('div[tabs]')), true, 'the tabs are the side\'s first row')
@@ -206,12 +206,12 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
206
206
  test('a tab shows its panel and hides the others without taking them down -- the verdict keeps what it holds', async () => {
207
207
  const panels = () =>
208
208
  read(() => [...screenCheck.editor().shadowRoot.querySelectorAll('aside > div[panel]')].map(p => ({ seen: p.getBoundingClientRect().height > 0, connected: p.isConnected, verdict: !!p.querySelector('slot[name=verdict]') })))
209
- const tabs = ['properties', 'motion', 'data', 'verdict']
209
+ const tabs = ['properties', 'response', 'motion', 'capability', 'data', 'verdict']
210
210
  for (const [at, tab] of tabs.entries()) {
211
211
  await pressTab(TAB(tab))
212
212
  await until(`${tab} on`, name => screenCheck.editor().shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === name, TAB(tab))
213
213
  const now = await panels()
214
- assert.equal(now.length, 4, 'four panels, all standing')
214
+ assert.equal(now.length, 6, 'six panels, all standing')
215
215
  assert.deepEqual(now.map(p => p.seen), tabs.map((_, i) => i === at), `${tab}: its panel seen, the rest hidden`)
216
216
  assert.ok(now.every(p => p.connected))
217
217
  assert.ok(now[tabs.indexOf('verdict')].verdict, 'the verdict panel holds the verdict slot whichever tab is on')
@@ -329,7 +329,7 @@ describe('the editor is drawn in the shell\'s tokens (2026-09-27)', () => {
329
329
  describe('a part chosen: each tab in its place, the figure-wide tabs give way, the list\'s root goes back', () => {
330
330
  before(() => mount('arm'))
331
331
 
332
- test('properties hold the part\'s size, place, material and, folded, its places; motion its motion and controls -- each under the part\'s head', async () => {
332
+ test('properties hold the part\'s size and place; 「반응형」 how it answers a size; capability its places; motion its motion and controls -- each under the part\'s head', async () => {
333
333
  await pressRow('upper-arm')
334
334
  await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
335
335
  const holds = () =>
@@ -349,16 +349,17 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
349
349
  assert.ok(now.subject, 'the part\'s head')
350
350
  assert.ok(now.links.includes('figure.label.group-size') && now.links.includes('figure.label.group-place'), `the shape's links: ${now.links}`)
351
351
  assert.ok(now.material, 'its material')
352
- /* What it does is a folded group of the properties now (chief architect 2026-10-16), its places inside. */
353
- await press('the capability fold', () => screenCheck.shownPanel().querySelector('section[capability] > details > summary'))
354
- const places = await read(() => {
355
- const fold = screenCheck.shownPanel().querySelector('section[capability] > details')
356
- const links = fold.querySelector('figure-links')
357
- return { open: fold.open, heads: links ? [...links.shadowRoot.querySelectorAll('section > h3')].map(h => screenCheck.ownText(h)) : [], capability: !!fold.querySelector('figure-capability-panel') }
358
- })
359
- assert.ok(places.open, 'the fold opened')
360
- assert.ok(places.heads.length === 1 && places.heads[0].startsWith('figure.label.places-on-part'), `the places on the part: ${places.heads}`)
361
- assert.ok(places.capability, 'its capabilities')
352
+ /* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」). */
353
+ await pressTab(TAB('response'))
354
+ now = await holds()
355
+ assert.ok(now.subject, 'the part\'s head')
356
+ assert.ok(now.links.includes('figure.label.group-size'), `the response's links: ${now.links}`)
357
+ /* What it does is a tab again. */
358
+ await pressTab(TAB('capability'))
359
+ now = await holds()
360
+ assert.ok(now.subject, 'the part\'s head')
361
+ assert.ok(now.links.length === 1 && now.links[0].startsWith('figure.label.places-on-part'), `the places on the part: ${now.links}`)
362
+ assert.ok(now.capability, 'its capabilities')
362
363
  await pressTab(TAB('motion'))
363
364
  now = await holds()
364
365
  assert.ok(now.subject, 'the part\'s head')
@@ -812,8 +813,11 @@ describe('a part\'s size row: a rule\'s numbers only where a rule sets every sid
812
813
  await mount('box')
813
814
  await pressRow('box')
814
815
  await until('box chosen', () => screenCheck.editor().selected === 'box')
815
- await press('follow the figure\'s size', () => screenCheck.deep('button[follow-size]')[0])
816
+ /* Following the figure's size is the 「반응형」 tab's; the size row it gives is the properties'. */
817
+ await pressTab(TAB('response'))
818
+ await press('follow the figure\'s size', () => screenCheck.deep('button[follow-size]').find(b => b.checkVisibility()))
816
819
  await until('following', () => ['x', 'y', 'z'].every(axis => !screenCheck.editor().session.read().fields.some(f => f.id === `box.size.${axis}`)))
820
+ await pressTab(TAB('properties'))
817
821
  assert.deepEqual(await sizeRow(), { held: 3 }, 'three sides, each held by the rule')
818
822
  })
819
823
  })
@@ -841,9 +845,12 @@ describe('the side panel is found as it was left, per part', () => {
841
845
  test('scrolled and a fold opened on one part; another part opens at its top; back, the first is as it was left', async () => {
842
846
  await pressRow('upper-arm')
843
847
  await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
848
+ /* 「더 보기」 starts open on this page: folded first, so the fold opened below is one this part leaves open. */
849
+ if (await read(() => !!screenCheck.shownPanel().querySelector('section[more] > details[open]')))
850
+ await press('the more fold', () => screenCheck.shownPanel().querySelector('section[more] > details > summary'))
844
851
  await press('a fold', () => {
845
852
  const panel = screenCheck.shownPanel()
846
- const closed = [...panel.querySelectorAll('figure-links')].flatMap(l => [...l.shadowRoot.querySelectorAll('details')]).find(d => !d.open)
853
+ const closed = [...panel.querySelectorAll('details'), ...[...panel.querySelectorAll('figure-links')].flatMap(l => [...l.shadowRoot.querySelectorAll('details')])].find(d => !d.open && d.checkVisibility())
847
854
  return closed?.querySelector('summary')
848
855
  })
849
856
  const opened = await folds()
@@ -1165,8 +1172,9 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1165
1172
  await open('fig-p', 'arm-clip')
1166
1173
  await pressRow('upper-arm')
1167
1174
  await until('upper-arm chosen', () => screenCheck.editor().selected === 'upper-arm')
1168
- /* The part's panel fits the side now; its places opened make it long enough to scroll. */
1169
- await press('the capability fold', () => screenCheck.shownPanel().querySelector('section[capability] > details:not([open]) > summary'))
1175
+ /* The part's panel fits the side now: its 「더 보기」 open makes it long enough to scroll. */
1176
+ if (await read(() => !!screenCheck.shownPanel().querySelector('section[more] > details:not([open])')))
1177
+ await press('the more fold', () => screenCheck.shownPanel().querySelector('section[more] > details:not([open]) > summary'))
1170
1178
  const at = await read(() => {
1171
1179
  const r = screenCheck.shownPanel().getBoundingClientRect()
1172
1180
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 }
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Words that send the author somewhere name a place that is there (chief architect's ruling 2026-10-17: six side tabs,
3
+ * 「반응형」 holding size behaviour, the position that follows the size, the row and the range). The modeller once said
4
+ * 「움직임 탭의 치수에서」 and 「「위치와 크기」에서 「여기에 고정」」 after both had moved, and 「성숙도 탭」 for the tab named 검사.
5
+ */
6
+ import { test } from 'node:test'
7
+ import assert from 'node:assert/strict'
8
+ import { readFileSync } from 'node:fs'
9
+
10
+ const table = (path: string) => JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf8')) as Record<string, string>
11
+ const GONE: Record<string, RegExp> = {
12
+ ko: /성숙도 탭|「위치와 크기」|움직임 탭의 (치수|붙이기)/,
13
+ en: /maturity tab|under placement|under size and placement|(sizes|fastening) in the motion tab/
14
+ }
15
+
16
+ test('no message sends the author to a tab or group the side no longer has', () => {
17
+ for (const lang of ['ko', 'en'] as const)
18
+ for (const path of ['../translations/', '../../figure-service/translations/']) {
19
+ const said = Object.entries(table(`${path}${lang}.json`)).filter(([, words]) => GONE[lang]!.test(words))
20
+ assert.deepEqual(said, [], `${path}${lang}.json`)
21
+ }
22
+ })
23
+
24
+ test('the places named are the tabs as they are called', () => {
25
+ const ko = table('../translations/ko.json')
26
+ const service = table('../../figure-service/translations/ko.json')
27
+ assert.ok(ko['figure.text.gizmo-size-follows']!.includes(`${ko['figure.label.tab-short-response']} 탭`))
28
+ assert.ok(ko['figure.text.preview-blocked-by-errors']!.includes(`${ko['figure.label.tab-short-verdict']} 탭`))
29
+ for (const key of ['figure.gate.parts-split-on-resize.how', 'figure.gate.sizing-not-uniform.how'])
30
+ assert.ok(service[key]!.includes(`${ko['figure.label.tab-short-response']} 탭의 「${ko['figure.label.group-position-response']}」`), key)
31
+ })
@@ -20,6 +20,9 @@
20
20
  "figure.label.group-menu": "More actions",
21
21
  "figure.label.group-more": "More",
22
22
  "figure.label.group-place": "Place",
23
+ "figure.label.group-position-response": "Position",
24
+ "figure.label.tab-response": "Responsive -- when the size changes",
25
+ "figure.label.tab-short-response": "Responsive",
23
26
  "figure.text.sizing-uniform-proposed": "this figure has joints. growing it by one ratio keeps the joints together at any size.",
24
27
  "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.",
25
28
  "figure.button.mirror-x": "across X",
@@ -75,9 +78,9 @@
75
78
  "figure.text.gizmo-joint-mimic": "the {joint} joint follows {leader}. move {leader}.",
76
79
  "figure.text.gizmo-joint-fixed-turn": "the {joint} joint does not turn. give it a turn in the motion tab.",
77
80
  "figure.text.gizmo-joint-turned-slide": "the {joint} joint only slides. use the move handle.",
78
- "figure.text.gizmo-joint-origin-follows": "the {joint} joint's place follows another part. change it under fastening in the motion tab.",
81
+ "figure.text.gizmo-joint-origin-follows": "the {joint} joint's place follows another part. change it under place in the properties tab.",
79
82
  "figure.text.gizmo-joint-off-axis": "the {joint} joint turns only about {axis}. use the {axis} rotate handle.",
80
- "figure.text.gizmo-size-follows": "{part}'s {axis} size follows another measure. change it under sizes in the motion tab.",
83
+ "figure.text.gizmo-size-follows": "{part}'s {axis} size follows another measure. change it under size in the responsive tab.",
81
84
  "figure.text.gizmo-fastening-places": "{part} is fastened to a face of another part, which places it. detach it first to move it.",
82
85
  "figure.text.instance-range-can-widen": "measured again with the parts as they are: {ranges} mm.",
83
86
  "figure.text.size-range-too-small": "below {mm} mm on {axis} the {part} part has no length, yet the figure is declared to be placed smaller, so this change cannot be made. use \"measure again\" under the sizes it can be placed at.",
@@ -472,7 +475,7 @@
472
475
  "figure.text.pick-a-shape-above-to-start": "pick a shape above to add the first part.",
473
476
  "figure.text.placement-explained": "sets the anchor plane in 3D scene (floor: AGV/conveyor, ceiling: OHT/hoist, center: sensor/drone).",
474
477
  "figure.text.pose-as-default-explained": "Writes the pose you have set as the parameters’ defaults, so the figure stands like this when nobody gives it a value.",
475
- "figure.text.preview-blocked-by-errors": "cannot show the preview because of format errors. see the maturity tab for the reason.",
478
+ "figure.text.preview-blocked-by-errors": "cannot show the preview because of format errors. see the check tab for the reason.",
476
479
  "figure.text.put-on-floor": "Moves the part once so its bottom is on the floor (Y=0). It does not stay there when the size changes; for that, use “Follow size, attached to the floor”.",
477
480
  "figure.text.range-needs-room": "the maximum must be above the minimum.",
478
481
  "figure.text.release-needs-a-save-first": "there are unsaved changes. save first.",
@@ -20,6 +20,9 @@
20
20
  "figure.label.group-menu": "Más acciones",
21
21
  "figure.label.group-more": "Más",
22
22
  "figure.label.group-place": "Posición",
23
+ "figure.label.group-position-response": "Posición",
24
+ "figure.label.tab-response": "Adaptable -- cuando cambia el tamaño",
25
+ "figure.label.tab-short-response": "Adaptable",
23
26
  "figure.text.sizing-uniform-proposed": "esta figura tiene articulaciones. escalarla con una sola proporción mantiene unidas las articulaciones en cualquier tamaño.",
24
27
  "figure.text.sizing-uniform-explained": "escala toda la figura según la media geométrica de los tres tamaños que le asigna un tablero. los pies quedan en el suelo; el espacio que sobra encima de la figura queda vacío.",
25
28
  "figure.button.mirror-x": "al otro lado de X",
@@ -20,6 +20,9 @@
20
20
  "figure.label.group-menu": "その他の操作",
21
21
  "figure.label.group-more": "その他",
22
22
  "figure.label.group-place": "位置",
23
+ "figure.label.group-position-response": "位置",
24
+ "figure.label.tab-response": "レスポンシブ — サイズが変わるとき",
25
+ "figure.label.tab-short-response": "レスポンシブ",
23
26
  "figure.text.sizing-uniform-proposed": "関節のある図形です。一つの比率で拡大すると、どのサイズでも関節が離れません。",
24
27
  "figure.text.sizing-uniform-explained": "ボードが与えた 3 軸サイズの幾何平均の比率で図形全体を拡大します。足は床に置き、箱の上の余りは空けたままにします。",
25
28
  "figure.button.mirror-x": "X 反対側に",
@@ -20,6 +20,9 @@
20
20
  "figure.label.group-menu": "다른 동작",
21
21
  "figure.label.group-more": "더 보기",
22
22
  "figure.label.group-place": "자리",
23
+ "figure.label.group-position-response": "위치",
24
+ "figure.label.tab-response": "반응형 — 크기가 바뀔 때",
25
+ "figure.label.tab-short-response": "반응형",
23
26
  "figure.text.sizing-uniform-proposed": "관절이 있는 도형입니다. 한 비율로 키우면 어떤 크기에서도 관절이 벌어지지 않습니다.",
24
27
  "figure.text.sizing-uniform-explained": "보드가 준 세 축 크기의 기하평균 비율로 도형 전체를 키웁니다. 발은 바닥에 두고, 상자 위가 남으면 그대로 비워 둡니다.",
25
28
  "figure.button.mirror-x": "X 반대편에",
@@ -75,9 +78,9 @@
75
78
  "figure.text.gizmo-joint-mimic": "{joint} 관절은 {leader} 관절을 따라 움직입니다. {leader}을(를) 움직이세요.",
76
79
  "figure.text.gizmo-joint-fixed-turn": "{joint} 관절은 돌지 않습니다. 돌리려면 움직임 탭에서 회전을 주세요.",
77
80
  "figure.text.gizmo-joint-turned-slide": "{joint} 관절은 미끄러지기만 합니다. 이동 손잡이로 움직이세요.",
78
- "figure.text.gizmo-joint-origin-follows": "{joint} 관절의 위치는 다른 부품을 기준으로 결정됩니다. 움직임 탭의 붙이기 설정에서 변경해 주세요.",
81
+ "figure.text.gizmo-joint-origin-follows": "{joint} 관절의 위치는 다른 부품을 기준으로 결정됩니다. 속성 탭의 「자리」에서 바꾸세요.",
79
82
  "figure.text.gizmo-joint-off-axis": "{joint} 관절은 {axis}축으로만 돕니다. {axis} 회전 손잡이를 쓰세요.",
80
- "figure.text.gizmo-size-follows": "{part}의 {axis} 치수는 다른 치수를 따라 정해집니다. 움직임 탭의 치수에서 바꾸세요.",
83
+ "figure.text.gizmo-size-follows": "{part}의 {axis} 치수는 다른 치수를 따라 정해집니다. 반응형 탭의 「크기」에서 바꾸세요.",
81
84
  "figure.text.gizmo-fastening-places": "{part} 부품은 다른 부품의 면에 부착되어 있습니다. 위치를 직접 변경하려면 부착 관계를 먼저 해제해 주세요.",
82
85
  "figure.text.instance-range-can-widen": "지금 부품으로 다시 재면 {ranges} mm 입니다.",
83
86
  "figure.text.size-range-too-small": "이 도형은 {axis} 축을 {mm} mm 보다 작게 하면 {part} 부품의 길이가 0 이 되는데, 그보다 작게 놓일 수 있다고 선언되어 있어 이 변경을 할 수 없습니다. 「놓일 수 있는 크기」에서 다시 재기를 누르면 범위가 맞춰집니다.",
@@ -472,7 +475,7 @@
472
475
  "figure.text.pick-a-shape-above-to-start": "위에서 도형을 선택해 첫 부품을 추가하세요.",
473
476
  "figure.text.placement-explained": "3D 씬에서 객체의 기준면을 정합니다 (바닥: AGV/컨베이어, 천장: OHT/호이스트, 중심: 센서/드론).",
474
477
  "figure.text.pose-as-default-explained": "현재 자세를 파라미터의 기본값으로 저장합니다. 값이 지정되지 않으면 도형에 이 자세가 적용됩니다.",
475
- "figure.text.preview-blocked-by-errors": "형식 오류로 미리보기를 표시할 수 없습니다. 성숙도 탭에서 사유를 확인하세요.",
478
+ "figure.text.preview-blocked-by-errors": "형식 오류로 미리보기를 표시할 수 없습니다. 검사 탭에서 사유를 확인하세요.",
476
479
  "figure.text.put-on-floor": "지금 위치만 바닥(Y=0)에 맞춥니다. 크기가 바뀌면 따라가지 않습니다. 계속 붙어 있게 하려면 ‘바닥에 붙여 크기 따라가기’를 쓰세요.",
477
480
  "figure.text.range-needs-room": "최대가 최소보다 커야 합니다.",
478
481
  "figure.text.release-needs-a-save-first": "저장하지 않은 변경이 있습니다. 먼저 저장하세요.",
@@ -20,6 +20,9 @@
20
20
  "figure.label.group-menu": "Tindakan lain",
21
21
  "figure.label.group-more": "Lagi",
22
22
  "figure.label.group-place": "Kedudukan",
23
+ "figure.label.group-position-response": "Kedudukan",
24
+ "figure.label.tab-response": "Responsif -- apabila saiz berubah",
25
+ "figure.label.tab-short-response": "Responsif",
23
26
  "figure.text.sizing-uniform-proposed": "figura ini mempunyai sendi. membesarkannya dengan satu nisbah memastikan sendi tidak terpisah pada sebarang saiz.",
24
27
  "figure.text.sizing-uniform-explained": "membesarkan seluruh figura dengan min geometri tiga saiz yang diberi papan. kaki kekal di lantai; ruang di atas figura dibiarkan kosong.",
25
28
  "figure.button.mirror-x": "seberang X",
@@ -20,6 +20,9 @@
20
20
  "figure.label.group-menu": "更多操作",
21
21
  "figure.label.group-more": "更多",
22
22
  "figure.label.group-place": "位置",
23
+ "figure.label.group-position-response": "位置",
24
+ "figure.label.tab-response": "响应式 — 尺寸变化时",
25
+ "figure.label.tab-short-response": "响应式",
23
26
  "figure.text.sizing-uniform-proposed": "这是带关节的图形。按单一比例放大,任何尺寸下关节都不会分开。",
24
27
  "figure.text.sizing-uniform-explained": "按看板给出的三轴尺寸的几何平均比例放大整个图形。底部保持在地面,盒子上方多出的空间保持空着。",
25
28
  "figure.button.mirror-x": "X 对侧",