@things-factory/figure-ui 10.1.175 → 10.1.176

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.175",
3
+ "version": "10.1.176",
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.170",
45
- "@things-factory/figure-service": "^10.1.172",
46
- "@things-factory/figure-viewer": "^10.1.175",
47
- "@things-factory/setting-base": "^10.1.167",
44
+ "@things-factory/ai-assistant": "^10.1.176",
45
+ "@things-factory/figure-service": "^10.1.176",
46
+ "@things-factory/figure-viewer": "^10.1.176",
47
+ "@things-factory/setting-base": "^10.1.176",
48
48
  "three": "^0.185.1"
49
49
  },
50
- "gitHead": "36a28986fdfb366ac31516b63369cf5733a82c31"
50
+ "gitHead": "24928f127d73de0e9946423bd4c3295f54cce03d"
51
51
  }
@@ -214,7 +214,8 @@ async function start() {
214
214
  }
215
215
  const properties = document.createElement('figure-properties')
216
216
  properties.slot = 'figure'
217
- const keep = () => (properties.source = editor.session.source())
217
+ /* As the modeller page gives it: the revision on screen (it gave the source, which the panel no longer reads -- N6). */
218
+ const keep = () => (properties.revision = editor.session.revisionNow())
218
219
  keep()
219
220
  editor.addEventListener('changed', keep)
220
221
  /* The figure panel's commands, as the modeller page runs them: one step of the session's history. */
@@ -40,6 +40,14 @@ for (const signal of ['SIGINT', 'SIGTERM'])
40
40
  * `standIns` maps a module the page would import to a file beside this one: for a module that cannot run here (the
41
41
  * assistant dock needs the whole app's tool bar), a stand-in that records what the product asked of it.
42
42
  */
43
+ /*
44
+ Packages that must stand once on a page: what defines custom elements (lit, the @operato and @material elements) and
45
+ what holds one state (i18next). A sibling checkout linked into node_modules for local work -- the operato
46
+ webcomponents, 2026-10-17 -- brought its own @operato/i18n, the page defined ox-i18n twice and stopped before any
47
+ check could start. Each is taken from this checkout's node_modules, whoever imports it.
48
+ */
49
+ const ONE_COPY = id => /^(lit|lit-html|lit-element|@lit\/|@lit-labs\/|i18next|@operato\/|@material\/)/.test(id)
50
+
43
51
  export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {}) {
44
52
  /*
45
53
  The real path: on macOS the temp dir is a link (/var -> /private/var). A module named both through
@@ -87,7 +95,7 @@ export async function buildPage(page = 'modeller-page.js', { standIns = {} } = {
87
95
  }
88
96
  },
89
97
  replace({ preventAssignment: true, values: { 'process.env.NODE_ENV': JSON.stringify('production') } }),
90
- nodeResolve({ browser: true, modulePaths: [join(PACKAGE, 'node_modules'), join(ROOT, 'node_modules')] }),
98
+ nodeResolve({ browser: true, modulePaths: [join(PACKAGE, 'node_modules'), join(ROOT, 'node_modules')], rootDir: PACKAGE, dedupe: ONE_COPY }),
91
99
  commonjs()
92
100
  ]
93
101
  })
@@ -131,6 +131,12 @@ async function press(selector, text, what = text ?? selector) {
131
131
  text
132
132
  )
133
133
  assert.ok(await element.evaluate(e => !!e), `${what} is on the page`)
134
+ /*
135
+ In the middle of the panel first, as a hand scrolls to what it presses: brought only just into view, at the panel's
136
+ top, it lay under the panel's sticky head and the press fell there (2026-10-17, the leaf's fasten-start once the
137
+ rows were tightened).
138
+ */
139
+ await element.evaluate(e => e.scrollIntoView({ block: 'center' }))
134
140
  await element.click()
135
141
  await settled()
136
142
  }
@@ -232,16 +232,19 @@ describe('the parts panel stands at the side\'s heights (2026-10-16)', () => {
232
232
  before(() => mount('rollers'))
233
233
 
234
234
  test('the detail level is one height with the side\'s fields, and the palette\'s tiles one height with each other', async () => {
235
+ await pick('bed')
235
236
  const heights = await read(() => {
236
237
  const parts = document.querySelector('figure-editor').shadowRoot.querySelector('figure-parts').shadowRoot
237
238
  const h = e => Math.round(e.getBoundingClientRect().height)
238
239
  const tiles = [...parts.querySelectorAll('div[palette] button[shape]:not([import])')].map(h)
239
240
  return { level: h(parts.querySelector('div[level] select')), tiles: [...new Set(tiles)], count: tiles.length, import: h(parts.querySelector('button[shape][import]')) }
240
241
  })
241
- assert.equal(heights.level, 30, 'the detail level select')
242
+ const side = await read(() => Math.round(document.querySelector('figure-editor').shadowRoot.querySelector('input[name-field]')?.getBoundingClientRect().height ?? 0))
243
+ assert.ok(side > 0, 'a side field to measure against')
244
+ assert.equal(heights.level, side, 'the detail level select, one height with the side\'s fields')
242
245
  assert.ok(heights.count >= 9, `the starting shapes: ${heights.count}`)
243
246
  assert.equal(heights.tiles.length, 1, `one tile height: ${heights.tiles}`)
244
- assert.equal(heights.import, 30, 'the file import on one line, its own row')
247
+ assert.equal(heights.import, side, 'the file import on one line, its own row, at the fields\' height')
245
248
  })
246
249
 
247
250
  /* A static guard sees the sizes written down; an icon with none stands at md-icon's own 24 (the recipe catalog's did). */
@@ -260,3 +263,38 @@ describe('the parts panel stands at the side\'s heights (2026-10-16)', () => {
260
263
  assert.deepEqual(sizes.filter(s => !['16', '18', '20'].includes(s.split(' ')[0])), [])
261
264
  })
262
265
  })
266
+
267
+ describe('a label and its field on one line, the field a line not a box (user 2026-10-17)', () => {
268
+ before(() => mount('rollers'))
269
+
270
+ /* Every visible label beside the control after it: the field's row, not the row above. */
271
+ const rows = () =>
272
+ read(() => {
273
+ const all = []
274
+ const walk = root => { for (const e of root.querySelectorAll('*')) { all.push(e); if (e.shadowRoot) walk(e.shadowRoot) } }
275
+ walk(document.querySelector('figure-editor').shadowRoot.querySelector('div[panel]:not([off])'))
276
+ const out = []
277
+ for (const field of all.filter(e => e.matches?.('div[field], div[inline-field], div[form]') && e.checkVisibility())) {
278
+ const label = field.querySelector(':scope > label, :scope > span')
279
+ const control = field.querySelector(':scope > input, :scope > select, :scope > div[swatches], :scope > div[row]')
280
+ if (!label || !control || !label.checkVisibility() || !control.checkVisibility()) continue
281
+ const a = label.getBoundingClientRect(), b = control.getBoundingClientRect()
282
+ out.push({ label: label.textContent.trim().slice(0, 20), sameLine: a.top < b.bottom && b.top < a.bottom, leftOf: a.right <= b.left + 1 })
283
+ }
284
+ const boxed = all.filter(e => e.matches?.('input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=color]), select') && e.checkVisibility() && !e.closest('div[cell]'))
285
+ .map(e => { const s = getComputedStyle(e); return { top: s.borderTopWidth, left: s.borderLeftWidth, bottom: s.borderBottomWidth } })
286
+ return { out, boxed }
287
+ })
288
+
289
+ /* The figure's own group is the modeller page's (figure-properties), not this page's: its rows are checked in screen.check. */
290
+ for (const part of ['bed', 'roller']) {
291
+ test(`${part}: every label beside its field, every field a line`, async () => {
292
+ await pick(part)
293
+ const { out, boxed } = await rows()
294
+ assert.ok(out.length >= 2, `rows read: ${out.length}`)
295
+ assert.deepEqual(out.filter(r => !r.sameLine || !r.leftOf), [], 'labels on their field\'s line, to its left')
296
+ assert.ok(boxed.length > 0, 'fields read')
297
+ assert.deepEqual(boxed.filter(b => b.top !== '0px' || b.left !== '0px' || b.bottom !== '1px'), [], 'only the line under it')
298
+ })
299
+ }
300
+ })
@@ -1008,6 +1008,28 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1008
1008
  }
1009
1009
  })
1010
1010
 
1011
+ /* The figure's own group in the side's one-line rows (user 2026-10-17): the identifier, the name, where it stands. */
1012
+ test('the figure panel\'s labels stand beside their fields, the identifier\'s too, each field a line', async () => {
1013
+ await pressTab(TAB('properties'))
1014
+ const rows = await read(() => {
1015
+ const properties = screenCheck.deep('figure-properties')[0]
1016
+ const out = []
1017
+ const type = properties.querySelector('label[type-field]')
1018
+ const pairs = [[type?.querySelector('span'), type?.querySelector('input, code')]]
1019
+ for (const field of properties.shadowRoot.querySelectorAll('div[field]')) pairs.push([field.querySelector(':scope > label'), field.querySelector(':scope > input, :scope > select, :scope > div[row]')])
1020
+ for (const [label, control] of pairs) {
1021
+ if (!label || !control || !label.checkVisibility() || !control.checkVisibility()) continue
1022
+ const a = label.getBoundingClientRect(), b = control.getBoundingClientRect()
1023
+ const line = control.matches('input, select') ? getComputedStyle(control) : null
1024
+ out.push({ label: label.textContent.trim(), sameLine: a.top < b.bottom && b.top < a.bottom, leftOf: a.right <= b.left + 1, box: line ? line.borderTopWidth !== '0px' || line.borderLeftWidth !== '0px' : false })
1025
+ }
1026
+ return out
1027
+ })
1028
+ assert.ok(rows.length >= 3, `rows read: ${rows.map(r => r.label)}`)
1029
+ assert.deepEqual(rows.filter(r => !r.sameLine || !r.leftOf), [], 'labels on their field\'s line, to its left')
1030
+ assert.deepEqual(rows.filter(r => r.box), [], 'fields drawn as a line, not a box')
1031
+ })
1032
+
1011
1033
  test('the figure\'s type is the first field of the figure panel, under its heading', async () => {
1012
1034
  await pressTab(TAB('properties'))
1013
1035
  const first = await read(() => {
@@ -24,7 +24,8 @@ import { buildPage } from './build.mjs'
24
24
  const HERE = dirname(fileURLToPath(import.meta.url))
25
25
  const bundle = await buildPage('acceptance-page.js')
26
26
  const dir = dirname(bundle)
27
- const ko = readFileSync(resolve(HERE, '../../translations/ko.json'))
27
+ /* The figure panel's words are figure-viewer's: both packages' Korean, as the app loads every module's. */
28
+ const ko = Buffer.from(JSON.stringify({ ...JSON.parse(readFileSync(resolve(HERE, '../../../figure-viewer/translations/ko.json'), 'utf-8')), ...JSON.parse(readFileSync(resolve(HERE, '../../translations/ko.json'), 'utf-8')) }))
28
29
  const APP = resolve(process.env.OPERATO_APP ?? resolve(HERE, '../../../../../../operato-application/packages/operato-figure'))
29
30
  /* The builder's ThemeOverridePlugin, run from the app root so its module order is the app's. */
30
31
  const themeFiles = JSON.parse(