@things-factory/figure-ui 10.1.174 → 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/client/modeller/editor-styles.ts +1 -1
- package/client/modeller/figure-links.ts +5 -3
- package/client/modeller/figure-parts.ts +35 -14
- package/client/modeller/figure-properties.ts +2 -2
- package/client/pages/figure-modeller-page.ts +34 -11
- package/dist-client/modeller/editor-styles.js +1 -1
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/figure-links.js +5 -3
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +35 -14
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-properties.js +2 -2
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.js +34 -11
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/browser/acceptance-page.js +2 -1
- package/test/browser/build.mjs +9 -1
- package/test/browser/links.check.mjs +6 -0
- package/test/browser/props.check.mjs +63 -4
- package/test/browser/screen.check.mjs +22 -0
- package/test/browser/serve-acceptance.mjs +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/figure-ui",
|
|
3
|
-
"version": "10.1.
|
|
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.
|
|
45
|
-
"@things-factory/figure-service": "^10.1.
|
|
46
|
-
"@things-factory/figure-viewer": "^10.1.
|
|
47
|
-
"@things-factory/setting-base": "^10.1.
|
|
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": "
|
|
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
|
-
|
|
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. */
|
package/test/browser/build.mjs
CHANGED
|
@@ -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
|
}
|
|
@@ -36,8 +36,8 @@ async function until(fn, args, what, ms = 8000) {
|
|
|
36
36
|
await sleep(50)
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
async function mount(kind) {
|
|
40
|
-
await read(kind => harness.mount(kind, null), kind)
|
|
39
|
+
async function mount(kind, templates = []) {
|
|
40
|
+
await read((kind, templates) => harness.mount(kind, null, templates), kind, templates)
|
|
41
41
|
await until(
|
|
42
42
|
() => {
|
|
43
43
|
try {
|
|
@@ -172,6 +172,11 @@ describe('a group\'s 「⋯」: its rarer actions, by the mouse or the keyboard'
|
|
|
172
172
|
await until(() => document.activeElement?.localName === 'ox-popup-menu', [], 'the menu open, with the focus')
|
|
173
173
|
const items = await read(() => [...document.querySelectorAll('ox-popup-menu[active] ox-popup-menuitem')].map(i => i.getAttribute('label')))
|
|
174
174
|
assert.deepEqual(items, ['figure.button.detach'])
|
|
175
|
+
/*
|
|
176
|
+
An item can take the focus once it has drawn (ox-popup-menuitem sets its tabindex in firstUpdated); an arrow before
|
|
177
|
+
that leaves the focus on the menu. A hand is never that quick: one in five runs here was (2026-10-17).
|
|
178
|
+
*/
|
|
179
|
+
await until(() => !!document.querySelector('ox-popup-menu[active] ox-popup-menuitem[tabindex]'), [], 'the item ready')
|
|
175
180
|
await page().keyboard.press('ArrowDown')
|
|
176
181
|
/* The item takes the focus on the menu's next update; Enter goes to it from then. */
|
|
177
182
|
await until(() => document.activeElement?.localName === 'ox-popup-menuitem', [], 'the item chosen')
|
|
@@ -227,15 +232,69 @@ describe('the parts panel stands at the side\'s heights (2026-10-16)', () => {
|
|
|
227
232
|
before(() => mount('rollers'))
|
|
228
233
|
|
|
229
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')
|
|
230
236
|
const heights = await read(() => {
|
|
231
237
|
const parts = document.querySelector('figure-editor').shadowRoot.querySelector('figure-parts').shadowRoot
|
|
232
238
|
const h = e => Math.round(e.getBoundingClientRect().height)
|
|
233
239
|
const tiles = [...parts.querySelectorAll('div[palette] button[shape]:not([import])')].map(h)
|
|
234
240
|
return { level: h(parts.querySelector('div[level] select')), tiles: [...new Set(tiles)], count: tiles.length, import: h(parts.querySelector('button[shape][import]')) }
|
|
235
241
|
})
|
|
236
|
-
|
|
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')
|
|
237
245
|
assert.ok(heights.count >= 9, `the starting shapes: ${heights.count}`)
|
|
238
246
|
assert.equal(heights.tiles.length, 1, `one tile height: ${heights.tiles}`)
|
|
239
|
-
assert.equal(heights.import,
|
|
247
|
+
assert.equal(heights.import, side, 'the file import on one line, its own row, at the fields\' height')
|
|
248
|
+
})
|
|
249
|
+
|
|
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). */
|
|
251
|
+
test('every icon the modeller shows stands in one of the three sizes, the ones with no size of their own too', async () => {
|
|
252
|
+
/* With a template the parts panel offers the recipe catalog -- the button whose icon had no size. */
|
|
253
|
+
await mount('rollers', [{ name: 'probe-template', target: 'part' }])
|
|
254
|
+
assert.ok(await read(() => !!document.querySelector('figure-editor').shadowRoot.querySelector('figure-parts').shadowRoot.querySelector('button[recipe-catalog]')), 'the recipe catalog offered')
|
|
255
|
+
const sizes = await read(() => {
|
|
256
|
+
const all = []
|
|
257
|
+
const walk = root => { for (const e of root.querySelectorAll('*')) { all.push(e); if (e.shadowRoot) walk(e.shadowRoot) } }
|
|
258
|
+
walk(document.querySelector('figure-editor').shadowRoot)
|
|
259
|
+
const icons = all.filter(e => e.localName === 'md-icon' && e.checkVisibility() && e.getBoundingClientRect().width > 0)
|
|
260
|
+
return icons.map(e => `${Math.round(e.getBoundingClientRect().width)} ${e.textContent.trim()}`)
|
|
261
|
+
})
|
|
262
|
+
assert.ok(sizes.length > 20, `icons seen: ${sizes.length}`)
|
|
263
|
+
assert.deepEqual(sizes.filter(s => !['16', '18', '20'].includes(s.split(' ')[0])), [])
|
|
240
264
|
})
|
|
241
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
|
-
|
|
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(
|