@things-factory/figure-ui 10.1.193 → 10.1.195

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 (45) hide show
  1. package/client/modeller/editor-styles.ts +10 -3
  2. package/client/modeller/editor.ts +61 -47
  3. package/client/modeller/figure-fasten-dialog.ts +11 -5
  4. package/client/modeller/refusal-teller.ts +8 -0
  5. package/client/pages/figure-list-page.ts +27 -40
  6. package/client/pages/figure-public-list-page.ts +21 -0
  7. package/client/route.ts +4 -0
  8. package/dist-client/modeller/editor-styles.js +10 -3
  9. package/dist-client/modeller/editor-styles.js.map +1 -1
  10. package/dist-client/modeller/editor.d.ts +18 -9
  11. package/dist-client/modeller/editor.js +62 -46
  12. package/dist-client/modeller/editor.js.map +1 -1
  13. package/dist-client/modeller/figure-fasten-dialog.d.ts +3 -3
  14. package/dist-client/modeller/figure-fasten-dialog.js +11 -5
  15. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  16. package/dist-client/modeller/refusal-teller.d.ts +8 -0
  17. package/dist-client/modeller/refusal-teller.js +7 -0
  18. package/dist-client/modeller/refusal-teller.js.map +1 -1
  19. package/dist-client/pages/figure-list-page.d.ts +6 -3
  20. package/dist-client/pages/figure-list-page.js +25 -42
  21. package/dist-client/pages/figure-list-page.js.map +1 -1
  22. package/dist-client/pages/figure-public-list-page.d.ts +16 -0
  23. package/dist-client/pages/figure-public-list-page.js +21 -0
  24. package/dist-client/pages/figure-public-list-page.js.map +1 -0
  25. package/dist-client/route.d.ts +1 -1
  26. package/dist-client/route.js +3 -0
  27. package/dist-client/route.js.map +1 -1
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +3 -3
  30. package/test/align.test.ts +13 -5
  31. package/test/browser/editing.check.mjs +9 -13
  32. package/test/browser/links.check.mjs +36 -22
  33. package/test/browser/list-page.js +35 -0
  34. package/test/browser/list.check.mjs +68 -0
  35. package/test/browser/modeller-page.js +48 -4
  36. package/test/browser/product-page.js +6 -0
  37. package/test/browser/props.check.mjs +2 -2
  38. package/test/figure-released.test.ts +1 -1
  39. package/things-factory.config.js +1 -0
  40. package/translations/en.json +4 -6
  41. package/translations/es.json +4 -6
  42. package/translations/ja.json +4 -6
  43. package/translations/ko.json +4 -6
  44. package/translations/ms.json +4 -6
  45. package/translations/zh.json +4 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.193",
3
+ "version": "10.1.195",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -18,7 +18,7 @@
18
18
  "scripts": {
19
19
  "build": "npm run build:server && npm run build:client",
20
20
  "test": "FIGURE_FREEZE_COMPILED=1 node --import ./test/resolve-ts-siblings.mjs --test test/*.test.ts",
21
- "test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs",
21
+ "test:browser": "node --test test/browser/build.check.mjs test/browser/modeller.check.mjs test/browser/modeller-revision.check.mjs test/browser/editing.check.mjs test/browser/links.check.mjs test/browser/section.check.mjs test/browser/screen.check.mjs test/browser/product-page.check.mjs test/browser/parts.check.mjs test/browser/places-mesh.check.mjs test/browser/part-ids.check.mjs test/browser/fix-alignment.check.mjs test/browser/mesh-select.check.mjs test/browser/row-preview.check.mjs test/browser/detail-tabs.check.mjs test/browser/props.check.mjs test/browser/retire.check.mjs test/browser/namespace.check.mjs test/browser/public.check.mjs test/browser/category.check.mjs test/browser/property-section.check.mjs test/browser/list.check.mjs",
22
22
  "copy:files": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
23
23
  "build:client": "npm run copy:files && npx tsc --p ./client/tsconfig.json",
24
24
  "build:server": "npx tsc --p ./server/tsconfig.json",
@@ -48,5 +48,5 @@
48
48
  "@things-factory/setting-base": "^10.1.192",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "193f04828369c453b43da82f4b6b46789696255f"
51
+ "gitHead": "e22e6dec8be22ba1d8db7bf511f3bacee721ff8f"
52
52
  }
@@ -95,13 +95,21 @@ test('a tilted part is told it touches by a point or an edge, a ball by its lowe
95
95
  assert.ok(!/기울|기운/.test(ko['figure.text.aligned-extreme-floor'] + ko['figure.text.align-to-floor-extreme']), 'a ball is not said to be tilted')
96
96
  })
97
97
 
98
- /* Designer 2026-09-27: the gizmo's reason for a tilted part named the floor's lowest point in every placement. */
99
- test('the size gizmo\'s reason names what the placement keeps, in every language', () => {
98
+ /*
99
+ User 2026-10-19: the size drag's refusal told the face kept while dragging and the one written, and was not understood.
100
+ One line, why only -- attached turned, or tilted -- in every language; the way on is the notice's button.
101
+ */
102
+ test('the size drag\'s refusal is one line saying why, in every language', () => {
100
103
  for (const lang of ['en', 'ja', 'ko', 'ms', 'zh', 'es']) {
101
104
  const t = JSON.parse(read(`../translations/${lang}.json`))
102
- assert.ok(t['figure.text.gizmo-size-turned-fastening'].includes('{extreme}'), lang)
103
- for (const placement of ['floor', 'ceiling', 'wall', 'center']) assert.ok(t[`figure.label.kept-extreme-${placement}`], `${lang} ${placement}`)
105
+ for (const key of ['figure.text.gizmo-size-turned-fastening', 'figure.text.gizmo-size-tilted']) {
106
+ assert.ok(t[key]?.includes('{part}'), `${lang} ${key}`)
107
+ assert.ok(!t[key].includes('{extreme}'), `${lang} ${key}: no face kept is told`)
108
+ assert.equal(t[key].split(/[.。]\s*/).filter(Boolean).length, 1, `${lang} ${key}: one sentence`)
109
+ }
110
+ assert.ok(t['figure.button.to-size-fields'], lang)
104
111
  }
105
112
  const ko = JSON.parse(read('../translations/ko.json'))
106
- assert.equal(ko['figure.label.kept-extreme-ceiling'], '가장 높은 점')
113
+ assert.equal(ko['figure.text.gizmo-size-turned-fastening'], '{part} 부품은 돌려 붙어 있어 끌어서 크기를 바꿀 수 없습니다.')
114
+ assert.equal(ko['figure.button.to-size-fields'], '크기 칸으로')
107
115
  })
@@ -532,31 +532,27 @@ describe('the recipe catalog says nothing where a template has no guide', () =>
532
532
  })
533
533
 
534
534
  /*
535
- The size gizmo's reason says what the figure's placement keeps (designer 2026-09-27). The page loads no translations;
536
- here the two sentences are given words, so the word the reason carries can be read.
535
+ The size gizmo withheld from a tilted part says why in one line (user 2026-10-19: the faces kept while dragging and
536
+ when written were not understood), as a notice, not the error's red. The page loads no translations; the sentence is
537
+ given words here, so the part it names can be read.
537
538
  */
538
- describe('the size gizmo withheld from a tilted part says what the placement keeps', () => {
539
+ describe('the size gizmo withheld from a tilted part says why, in one line, as a notice', () => {
539
540
  before(async () => {
540
541
  await mount('box', 'tilted-ceiling')
541
- await read(() =>
542
- editing.words({
543
- 'figure.text.gizmo-size-turned-fastening': '{part} is tilted: its size is typed, keeping {extreme}',
544
- 'figure.label.kept-extreme-ceiling': 'its highest point',
545
- 'figure.label.kept-extreme-floor': 'its lowest point'
546
- })
547
- )
542
+ await read(() => editing.words({ 'figure.text.gizmo-size-tilted': '{part} is tilted, so its size cannot be dragged.' }))
548
543
  await pick('box')
549
544
  })
550
545
  after(() => read(() => editing.words(null)))
551
546
 
552
- test('hung from the ceiling, the tooltip and the refusal of R both name its highest point', async () => {
547
+ test('hung from the ceiling, the tooltip and the refusal of R say the same line; nothing is said of the faces it keeps', async () => {
553
548
  const resize = (await read(() => editing.tools())).find(t => t.label === 'figure.button.gizmo-scale')
554
549
  assert.equal(resize.disabled, true, 'not offered')
555
- assert.ok(resize.title.endsWith('box is tilted: its size is typed, keeping its highest point'), resize.title)
550
+ assert.ok(resize.title.endsWith('box is tilted, so its size cannot be dragged.'), resize.title)
556
551
  await read(() => editing.told())
557
552
  await press('KeyR')
558
- assert.deepEqual((await read(() => editing.told())).filter(n => n.level === 'error').map(n => n.message), ['box is tilted: its size is typed, keeping its highest point'])
553
+ assert.deepEqual(await read(() => editing.told()), [{ level: 'info', message: 'box is tilted, so its size cannot be dragged.' }])
559
554
  assert.equal((await state()).attached, null, 'no gizmo on it')
555
+ assert.equal((await state()).error, '', 'no error')
560
556
  })
561
557
  })
562
558
 
@@ -370,7 +370,7 @@ describe('the fastening window on the robot arm', () => {
370
370
 
371
371
  test('the parts hanging from it are said in one line, and the chips show the part names as they are written', async () => {
372
372
  const said = await read(() => {
373
- const root = document.querySelector('figure-fasten-dialog').shadowRoot
373
+ const root = window.fastenDialog().shadowRoot
374
374
  const chip = root.querySelector('ox-input-select-buttons').shadowRoot.querySelector('md-filter-chip')
375
375
  const label = chip.shadowRoot.querySelector('.label-text') ?? chip.shadowRoot.querySelector('.label')
376
376
  return { lines: root.querySelectorAll('p[loops]').length, transform: getComputedStyle(label).textTransform }
@@ -388,14 +388,17 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
388
388
  })
389
389
  const ghosts = () => read(() => look.drawn().filter(n => n.startsWith('__preview')))
390
390
 
391
- test('started, the window opens in the standard popup, asking what to fasten to, nothing chosen', async () => {
391
+ test('started, the window opens over the side panel, asking what to fasten to, nothing chosen; the canvas stays in sight', async () => {
392
392
  await pick('leaf')
393
393
  await press('button[fasten-start]')
394
394
  const list = await until(() => probe.fastenTargets(), [], 'the fastening window')
395
395
  assert.deepEqual(list.filter(t => t.selected), [], 'nothing chosen')
396
396
  assert.ok(list.some(t => t.target === 'frame' && !t.disabled), 'the frame offered')
397
397
  assert.deepEqual((await all('[apply]')).map(b => b.disabled), [true], 'nothing to apply yet')
398
- assert.deepEqual(await read(() => probe.fastenPopup()), { title: 'figure.label.fastening', movable: true, backdrop: true, role: 'dialog' })
398
+ /* Not the modal popup (user 2026-10-19: it covered the 3D it was choosing for): over the side panel, at most its edge over the canvas. */
399
+ const panel = await read(() => probe.fastenPanel())
400
+ assert.deepEqual({ ...panel, overCanvas: undefined }, { title: 'figure.label.fastening', role: 'dialog', popup: false, overCanvas: undefined })
401
+ assert.ok(panel.overCanvas <= 0.1, `the canvas stays in sight: ${panel.overCanvas} of its width covered`)
399
402
  })
400
403
 
401
404
  test('the frame chosen: the editor is told the command, and the canvas draws where the leaf would go and the face it meets', async () => {
@@ -421,14 +424,15 @@ describe('a new fastening for a door leaf: chosen in the window, seen before it
421
424
  assert.deepEqual(await ghosts(), [], 'nothing of the earlier result drawn')
422
425
  })
423
426
 
424
- test('a press outside the window closes it with nothing written, and nothing it tried is drawn', async () => {
427
+ test('another part picked in the list closes the window with nothing written, and nothing it tried is drawn', async () => {
425
428
  const start = await state()
426
- /* The motion tab, under the popup's backdrop: the press closes the popup, it does not reach the tab. */
427
- await click(await read(l => probe.tab(l), 'figure.label.tab-short-motion'), 'outside the window')
429
+ /* No backdrop over the page: the parts list stays reachable. The window stands over the side panel, its tabs too, as the view settings do. */
430
+ assert.ok((await read(() => probe.fastenPanel())).overCanvas <= 0.1)
431
+ await pick('frame')
428
432
  await until(() => !probe.fastenTargets(), [], 'the window closed')
429
433
  await until(() => !look.drawn().some(n => n.startsWith('__preview')), [], 'nothing it tried drawn')
430
- assert.equal(await read(() => look.tab()), 'figure.label.tab-short-properties', 'the press did not reach the tab')
431
434
  assert.equal((await state()).revision, start.revision, 'nothing written')
435
+ await pick('leaf')
432
436
  })
433
437
 
434
438
  test('"fasten it first" from the motion tab opens this part\'s window, on what it has, on the properties tab', async () => {
@@ -599,23 +603,32 @@ function twisted() {
599
603
  }
600
604
 
601
605
  describe('a part fastened twisted is not sized by a drag', () => {
602
- test('the resize tool is off, and a size drag that still arrives is not written and is said', async () => {
606
+ test('the resize tool is off, and a size drag that still arrives is not written: a notice says why, in one line, with the way to its size', async () => {
603
607
  await mount('twisted', twisted())
604
608
  await pick('blade')
605
609
  assert.equal((await at('button[tool]', 'figure.button.gizmo-scale'))?.disabled, true, 'the resize tool is off')
610
+ await read(() => probe.told())
606
611
  const start = await state()
607
612
  await read(() => look.transformed('blade', { scale: [1.5, 1, 1] }))
608
613
  await settled()
609
614
  assert.equal((await state()).revision, start.revision, 'not written')
610
- assert.equal((await state()).error, 'figure.text.gizmo-size-turned-fastening')
615
+ /* A notice, not the error's red (user 2026-10-19: the reason read as a fault, and was not understood). */
616
+ assert.deepEqual(await read(() => probe.told()), [{ level: 'info', message: 'figure.text.gizmo-size-turned-fastening', action: 'figure.button.to-size-fields' }])
617
+ assert.equal((await state()).error, '', 'no error')
611
618
  })
612
619
 
613
- test('the R key says why', async () => {
620
+ test('the R key says why, and the notice\'s button puts the cursor in the part\'s size field', async () => {
614
621
  await mount('twisted', twisted())
615
622
  await pick('blade')
616
- assert.equal((await state()).error, '')
623
+ await read(() => probe.told())
617
624
  await page().keyboard.press('KeyR')
618
- await until(() => probe.state().error === 'figure.text.gizmo-size-turned-fastening', [], 'the reason said')
625
+ await until(() => probe.told().some(n => n.message === 'figure.text.gizmo-size-turned-fastening' && n.action === 'figure.button.to-size-fields'), [], 'the reason said with its way on')
626
+ assert.equal((await state()).error, '', 'no error')
627
+ /* From another tab: the button brings the properties tab and the size field. */
628
+ await tab('motion')
629
+ assert.equal(await read(() => probe.pressNoticeButton()), true)
630
+ await until(() => probe.focused()?.label?.startsWith('blade.size.'), [], 'the blade\'s size field focused')
631
+ assert.equal(await read(() => look.tab()), 'figure.label.tab-short-properties')
619
632
  })
620
633
  })
621
634
 
@@ -741,26 +754,27 @@ describe('the fastening window: tried freely, written once', () => {
741
754
  await until(w => JSON.stringify(JSON.parse(document.querySelector('figure-editor').session.source()).joints.find(j => j.body1.part === 'lamp')) === w, [JSON.stringify(was)], 'one undo: the fastening as it was')
742
755
  })
743
756
 
744
- test('moved by the popup\'s head out of the way of the canvas: nothing written, still open', async () => {
757
+ test('the canvas is turned with the window open: nothing written, still open, the window where it was', async () => {
745
758
  await pick('lamp')
746
759
  await changeFastening()
747
760
  const rect = () => read(() => {
748
- const r = document.querySelector('figure-fasten-dialog')?.getBoundingClientRect()
761
+ const r = window.fastenDialog()?.getBoundingClientRect()
749
762
  return r ? { x: Math.round(r.left), y: Math.round(r.top) } : null
750
763
  })
751
764
  const was = await rect()
752
- const head = await read(() => {
753
- const h = document.querySelector('figure-fasten-dialog').closest('ox-floating-overlay').shadowRoot.querySelector('[header] h1').getBoundingClientRect()
754
- return { x: h.left + h.width / 2, y: h.top + h.height / 2 }
765
+ /* A drag on the empty canvas, left of the parts: the camera turns, nothing is picked. */
766
+ const box = await read(() => {
767
+ const r = document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').getBoundingClientRect()
768
+ return { x: r.left + 40, y: r.top + r.height / 2 }
755
769
  })
756
770
  const start = await state()
757
- await page().mouse.move(head.x, head.y)
771
+ await page().mouse.move(box.x, box.y)
758
772
  await page().mouse.down()
759
- for (let i = 1; i <= 6; i++) await page().mouse.move(head.x - 30 * i, head.y + 20 * i)
773
+ for (let i = 1; i <= 6; i++) await page().mouse.move(box.x + 20 * i, box.y + 5 * i)
760
774
  await page().mouse.up()
761
- const moved = await rect()
762
- assert.deepEqual([moved.x - was.x, moved.y - was.y], [-180, 120], `moved with the pointer: ${JSON.stringify([was, moved])}`)
775
+ await settled()
763
776
  assert.equal(await windowOpen(), true, 'still open')
777
+ assert.deepEqual(await rect(), was, 'the window where it was')
764
778
  assert.equal((await state()).revision, start.revision, 'nothing written')
765
779
  await press('[cancel]')
766
780
  await until(() => !probe.fastenTargets(), [], 'the window closed')
@@ -771,7 +785,7 @@ describe('the fastening window: tried freely, written once', () => {
771
785
  await changeFastening()
772
786
  const litAt = y => Math.abs((document.querySelector('figure-editor').shadowRoot.querySelector('figure-canvas').highlight?.pose.t[1] ?? 0) - y) < 2
773
787
  /* The list has the focus, on the top it is fastened to. */
774
- await until(() => document.querySelector('figure-fasten-dialog')?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot.activeElement?.getAttribute('data-value') === 'top', [], 'the focus on the chosen chip')
788
+ await until(() => window.fastenDialog()?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot.activeElement?.getAttribute('data-value') === 'top', [], 'the focus on the chosen chip')
775
789
  await page().keyboard.press('ArrowLeft')
776
790
  /* The leg's top, 700 high (the lit plate a little over it); moving the focus lights, it does not choose. */
777
791
  await until(litAt, [702], 'the leg\'s top lit')
@@ -0,0 +1,35 @@
1
+ /*
2
+ * The catalogue and the public figures, as the rail opens them (user 2026-10-19): the domain's own figures with the
3
+ * new-figure button and the draft · released choice, and other domains' public figures with neither. The server is a
4
+ * stand-in that answers each list with one card and says which list was asked for.
5
+ */
6
+ import 'figure-ui-client/pages/figure-list-page.js'
7
+ import 'figure-ui-client/pages/figure-public-list-page.js'
8
+ import { client } from '@operato/graphql'
9
+
10
+ const asked = []
11
+ const card = { id: 'one', name: 'Mixer', type: 'mixer', namespace: 'acme.example', state: 'released', version: 1, public: true, tags: [] }
12
+ client.query = async ({ query }) => {
13
+ const field = query.definitions.find(d => d.kind === 'OperationDefinition')?.selectionSet.selections[0]?.name.value
14
+ asked.push(field)
15
+ if (field === 'figures' || field === 'publicFigures')
16
+ return { data: { [field]: { items: [field === 'publicFigures' ? { ...card, domain: { id: 'd2', name: 'Other works', subdomain: 'other' } } : card], total: 1 } } }
17
+ /* No kept preferences: the page as it first opens. */
18
+ if (field === 'myPageAllPreferences') return { data: { myPageAllPreferences: [] } }
19
+ return { data: {} }
20
+ }
21
+ client.mutate = async () => ({ data: {} })
22
+
23
+ window.harness = {
24
+ asked,
25
+ mount(tag) {
26
+ document.body.replaceChildren()
27
+ const page = document.createElement(tag)
28
+ page.style.cssText = 'display:block;height:800px'
29
+ /* What the shell gives a page it routes to: the page's name, read for its kept preferences. */
30
+ page.lifecycle = { page: tag === 'figure-public-list-page' ? 'figure-public-list' : 'figure-list' }
31
+ document.body.append(page)
32
+ page.active = true
33
+ return page.localName
34
+ }
35
+ }
@@ -0,0 +1,68 @@
1
+ /*
2
+ * The rail's two lists (user 2026-10-19): the catalogue is where figures are made and changed, the public figures of
3
+ * other domains only looked at. Each is a screen of its own; the catalogue has no switch between them, and the public
4
+ * list offers no new figure and no draft · released choice.
5
+ *
6
+ * Run: node --test test/browser/list.check.mjs
7
+ */
8
+ import { test, describe, before, after } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+
11
+ import { buildPage } from './build.mjs'
12
+ import { openPage } from './serve.mjs'
13
+
14
+ const sleep = ms => new Promise(done => setTimeout(done, ms))
15
+
16
+ let opened
17
+ before(async () => {
18
+ opened = await openPage(await buildPage('list-page.js'), { width: 1400, height: 900 })
19
+ })
20
+ after(async () => {
21
+ await opened?.close()
22
+ })
23
+
24
+ const page = () => opened.page
25
+ const read = (fn, ...args) => page().evaluate(fn, ...args)
26
+ async function until(fn, what, ms = 15000) {
27
+ const deadline = Date.now() + ms
28
+ for (;;) {
29
+ if (await read(fn).catch(() => false)) return
30
+ if (Date.now() > deadline) assert.fail(`waited for ${what}`)
31
+ await sleep(50)
32
+ }
33
+ }
34
+ /* What a list page shows: its title, its FAB, the choices in its filter bar, a scope switch, and the list it asked for. */
35
+ const look = () =>
36
+ read(() => {
37
+ const host = document.body.firstElementChild
38
+ const root = host.shadowRoot
39
+ const deep = (s, r = root, o = []) => {
40
+ o.push(...r.querySelectorAll(s))
41
+ for (const e of r.querySelectorAll('*')) if (e.shadowRoot) deep(s, e.shadowRoot, o)
42
+ return o
43
+ }
44
+ return {
45
+ title: host.context.title,
46
+ fab: !!root.querySelector('button[fab]'),
47
+ scopeSwitch: !!root.querySelector('button[scope]'),
48
+ stateChoice: deep('ox-input-select-buttons[name=state]').length,
49
+ asked: harness.asked.at(-1)
50
+ }
51
+ })
52
+
53
+ describe('the rail\'s two lists', () => {
54
+ test('the catalogue: its own figures, the new-figure button and the draft · released choice, no switch to the public ones', async () => {
55
+ await read(() => harness.mount('figure-list-page'))
56
+ await until(() => harness.asked.includes('figures'), 'the own list asked for')
57
+ await until(() => !!document.body.firstElementChild.shadowRoot.querySelector('ox-grist'), 'the grid')
58
+ await sleep(500)
59
+ assert.deepEqual(await look(), { title: 'figure.title.figures', fab: true, scopeSwitch: false, stateChoice: 1, asked: 'figures' })
60
+ })
61
+
62
+ test('the public figures: other domains\' list, no new figure, no draft · released choice', async () => {
63
+ await read(() => harness.mount('figure-public-list-page'))
64
+ await until(() => harness.asked.includes('publicFigures'), 'the public list asked for')
65
+ await sleep(500)
66
+ assert.deepEqual(await look(), { title: 'figure.title.public-figures', fab: false, scopeSwitch: false, stateChoice: 0, asked: 'publicFigures' })
67
+ })
68
+ })
@@ -87,6 +87,20 @@ function deep(selector, root = document) {
87
87
 
88
88
  const editor = () => document.querySelector('figure-editor')
89
89
  const canvas = () => deep('figure-canvas')[0]
90
+ /** The fastening window, wherever the editor draws it. */
91
+ window.fastenDialog = () => deep('figure-fasten-dialog')[0] ?? null
92
+
93
+ /*
94
+ * What the app's notification surface is handed (`notify`), in order. The page draws no snackbar, so a notice's button
95
+ * is pressed here, as the snackbar's would call it.
96
+ */
97
+ const notices = []
98
+ const pressable = []
99
+ document.addEventListener('notify', e => {
100
+ notices.push(e.detail)
101
+ const action = e.detail?.option?.action
102
+ if (action?.callback) pressable.push(action.callback)
103
+ })
90
104
  const three = () => canvas().scene.root._threeCapability
91
105
  const controls = () => three().gizmoManager.controls
92
106
 
@@ -218,10 +232,40 @@ window.probe = {
218
232
  const chip = deep('figure-fasten-dialog')[0]?.shadowRoot.querySelector('ox-input-select-buttons')?.shadowRoot?.querySelector(`md-filter-chip[data-value="${CSS.escape(part)}"]`)
219
233
  return chip ? centreOf(chip) : null
220
234
  },
221
- /** The fastening window's popup, as the standard overlay holds it: its title, whether its head moves it, its backdrop. */
222
- fastenPopup() {
223
- const overlay = deep('figure-fasten-dialog')[0]?.closest('ox-floating-overlay')
224
- return overlay ? { title: overlay.title, movable: overlay.movable, backdrop: !!overlay.backdrop, role: overlay.querySelector('figure-fasten-dialog').getAttribute('role') } : null
235
+ /**
236
+ * The fastening window as it stands: its title, its role, whether a popup or its backdrop is anywhere on the page, and
237
+ * how much of the canvas it covers (a share of the canvas's width).
238
+ */
239
+ fastenPanel() {
240
+ const dialog = deep('figure-fasten-dialog')[0]
241
+ const panel = dialog?.closest('[fasten-panel]')
242
+ if (!panel) return null
243
+ const p = panel.getBoundingClientRect(),
244
+ c = canvas().getBoundingClientRect()
245
+ const across = Math.max(0, Math.min(p.right, c.right) - Math.max(p.left, c.left))
246
+ return {
247
+ title: panel.querySelector('h3')?.textContent.trim(),
248
+ role: dialog.getAttribute('role'),
249
+ popup: deep('ox-floating-overlay').length > 0,
250
+ overCanvas: Math.round((across / c.width) * 100) / 100
251
+ }
252
+ },
253
+ /** What the notification surface was handed since the last call: its level, its words, its button's label. */
254
+ told() {
255
+ return notices.splice(0).map(n => ({ level: n.level, message: n.message, action: n.option?.action?.label ?? null }))
256
+ },
257
+ /** The button of the last notice that had one, pressed. */
258
+ pressNoticeButton() {
259
+ const last = [...pressable].pop()
260
+ if (!last) return false
261
+ last()
262
+ return true
263
+ },
264
+ /** The element that has the focus, through shadow roots: its tag and aria-label. */
265
+ focused() {
266
+ let at = document.activeElement
267
+ while (at?.shadowRoot?.activeElement) at = at.shadowRoot.activeElement
268
+ return at ? { tag: at.localName, label: at.getAttribute('aria-label') } : null
225
269
  },
226
270
  /** The page point of an input by its aria-label, or of the part name field. */
227
271
  nameField() {
@@ -155,6 +155,12 @@ function shellMoves(move) {
155
155
  shellMoving = true
156
156
  try {
157
157
  move()
158
+ /*
159
+ The address layer adopts a raw History write the next time the address is read or written, and tells its
160
+ subscribers then (@operato/utils 10.29.43, \`adoptForeignWrite\`). Read it here, so the shell's own move is
161
+ adopted while it is still known as the shell's -- not at the product's next step, recorded as the product's.
162
+ */
163
+ void addressController().entry
158
164
  } finally {
159
165
  shellMoving = false
160
166
  }
@@ -184,11 +184,11 @@ describe('a part\'s groups in one order, whatever its state', () => {
184
184
  })
185
185
  assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false })
186
186
  await press('button[change-fastening]', '「바꾸기」')
187
- await until(() => !!document.querySelector('figure-fasten-dialog'), [], 'the window')
187
+ await until(() => !!window.fastenDialog(), [], 'the window')
188
188
  assert.deepEqual(await place(), { sentence: true, change: true, drawing: false, cells: false }, 'the panel still says the result')
189
189
  /* Closed with nothing written; another part chosen and back: one line. */
190
190
  await page().keyboard.press('Escape')
191
- await until(() => !document.querySelector('figure-fasten-dialog'), [], 'the window closed')
191
+ await until(() => !window.fastenDialog(), [], 'the window closed')
192
192
  await pick('bed')
193
193
  await pick('mark')
194
194
  assert.equal((await place()).change, true, 'said in one line again')
@@ -49,6 +49,6 @@ test("another domain's public figure: its card offers no delete and names its do
49
49
  assert.match(thumb, /badge=\$\{this\.readonly \? figure\.domain\?\.name/)
50
50
  for (const lang of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
51
51
  const words = JSON.parse(read(`../translations/${lang}.json`))
52
- for (const key of ['figure.label.public', 'figure.button.make-public', 'figure.button.make-private', 'figure.button.public-figures']) assert.ok(words[key], `${lang} ${key}`)
52
+ for (const key of ['figure.label.public', 'figure.button.make-public', 'figure.button.make-private', 'figure.title.public-figures']) assert.ok(words[key], `${lang} ${key}`)
53
53
  }
54
54
  })
@@ -5,6 +5,7 @@ export default {
5
5
  route,
6
6
  routes: [
7
7
  { tagname: 'figure-list-page', page: 'figure-list' },
8
+ { tagname: 'figure-public-list-page', page: 'figure-public-list' },
8
9
  /* The shell builds the DOM from this table. */
9
10
  { tagname: 'figure-modeller-page', page: 'figure-modeller' }
10
11
  ],
@@ -507,6 +507,7 @@
507
507
  "figure.text.what-a-figure-is": "a Figure is a 3D component you author. compose it from simple shapes and it becomes a component you can place on a drawing, sharing shape and material across every instance.",
508
508
  "figure.text.what-changed-in-this-release": "what changed in this release",
509
509
  "figure.title.figures": "figure catalog",
510
+ "figure.title.public-figures": "public figures",
510
511
  "figure.title.new-figure": "new figure",
511
512
  "figure.title.save-new-figure": "save figure draft",
512
513
  "figure.text.no-parts-yet": "No parts yet. Pick a shape above to place one.",
@@ -842,11 +843,9 @@
842
843
  "figure.text.held-on-part": "Attached by its {face} to the {otherFace} of {other}",
843
844
  "figure.button.open-fastening": "Open attachment settings",
844
845
  "figure.button.detach-held": "Detach",
845
- "figure.text.gizmo-size-turned-fastening": "{part} is turned off its plane or attached twisted, so the face kept while dragging and the one kept when written would differ. Type the size in its field instead -- the placement's face (or {extreme}) stays where it is.",
846
- "figure.label.kept-extreme-floor": "lowest point",
847
- "figure.label.kept-extreme-ceiling": "highest point",
848
- "figure.label.kept-extreme-wall": "backmost point",
849
- "figure.label.kept-extreme-center": "centre",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} is attached turned, so its size cannot be dragged.",
847
+ "figure.text.gizmo-size-tilted": "{part} is tilted, so its size cannot be dragged.",
848
+ "figure.button.to-size-fields": "Go to size",
850
849
  "figure.label.align-input": "Follows its field ({field})",
851
850
  "figure.label.tab-short-properties": "Properties",
852
851
  "figure.label.tab-short-capability": "Capability",
@@ -1207,7 +1206,6 @@
1207
1206
  "figure.label.public": "public",
1208
1207
  "figure.button.make-public": "make public",
1209
1208
  "figure.button.make-private": "make private",
1210
- "figure.button.public-figures": "public figures of other domains",
1211
1209
  "figure.text.category-choose": "choose a category",
1212
1210
  "figure.text.property-section-failed": "the section {section} could not be drawn: {reason}"
1213
1211
  }
@@ -507,6 +507,7 @@
507
507
  "figure.text.what-a-figure-is": "una Figure es un componente 3D que usted crea. compóngala a partir de formas sencillas y se convierte en un componente que puede colocar en un plano, compartiendo forma y material entre todas las instancias.",
508
508
  "figure.text.what-changed-in-this-release": "qué cambió en esta publicación",
509
509
  "figure.title.figures": "catálogo de figuras",
510
+ "figure.title.public-figures": "figuras públicas",
510
511
  "figure.title.new-figure": "nueva figura",
511
512
  "figure.title.save-new-figure": "guardar borrador de figura",
512
513
  "figure.text.no-parts-yet": "Aún no hay piezas. Elija una forma arriba para colocar una.",
@@ -842,11 +843,9 @@
842
843
  "figure.text.held-on-part": "Unida por su {face} a {otherFace} de {other}",
843
844
  "figure.button.open-fastening": "Abrir la configuración de unión",
844
845
  "figure.button.detach-held": "Separar",
845
- "figure.text.gizmo-size-turned-fastening": "{part} está girada fuera de su plano o unida con torsión, por lo que la cara que se mantiene al arrastrar y la que se mantiene al guardar serían distintas. Escriba el tamaño en su campo; la cara de la colocación (o {extreme}) se queda donde está.",
846
- "figure.label.kept-extreme-floor": "punto más bajo",
847
- "figure.label.kept-extreme-ceiling": "punto más alto",
848
- "figure.label.kept-extreme-wall": "punto más trasero",
849
- "figure.label.kept-extreme-center": "centro",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} está unida girada, así que su tamaño no se puede arrastrar.",
847
+ "figure.text.gizmo-size-tilted": "{part} está inclinada, así que su tamaño no se puede arrastrar.",
848
+ "figure.button.to-size-fields": "Ir al tamaño",
850
849
  "figure.label.align-input": "Sigue su campo ({field})",
851
850
  "figure.label.tab-short-properties": "Propiedades",
852
851
  "figure.label.tab-short-capability": "Capacidad",
@@ -1207,7 +1206,6 @@
1207
1206
  "figure.label.public": "pública",
1208
1207
  "figure.button.make-public": "hacer pública",
1209
1208
  "figure.button.make-private": "dejar de publicar",
1210
- "figure.button.public-figures": "figuras públicas de otros dominios",
1211
1209
  "figure.text.category-choose": "elegir categoría",
1212
1210
  "figure.text.property-section-failed": "no se pudo dibujar la sección {section}: {reason}"
1213
1211
  }
@@ -507,6 +507,7 @@
507
507
  "figure.text.what-a-figure-is": "a Figure is a 3D component you author. Compose it from simple shapes and it becomes a component you can place on a drawing, sharing shape and material across every instance.",
508
508
  "figure.text.what-changed-in-this-release": "このバージョンで変わったこと",
509
509
  "figure.title.figures": "Figure カタログ",
510
+ "figure.title.public-figures": "公開 Figure",
510
511
  "figure.title.new-figure": "new figure",
511
512
  "figure.title.save-new-figure": "Figure の下書きを保存",
512
513
  "figure.text.no-parts-yet": "No parts yet. Pick a shape above to place one.",
@@ -842,11 +843,9 @@
842
843
  "figure.text.held-on-part": "{other}の{otherFace}に{face}で取り付け",
843
844
  "figure.button.open-fastening": "取り付け設定を開く",
844
845
  "figure.button.detach-held": "取り付けを外す",
845
- "figure.text.gizmo-size-turned-fastening": "{part} は傾けて回転しているか、ひねって取り付けられているため、ドラッグ中に見える面と確定する面が異なります。ギズモではなくサイズ欄に数値を入力してください。配置が保つ面(または{extreme})はそのまま残ります。",
846
- "figure.label.kept-extreme-floor": "最も低い点",
847
- "figure.label.kept-extreme-ceiling": "最も高い点",
848
- "figure.label.kept-extreme-wall": "最も奥の点",
849
- "figure.label.kept-extreme-center": "中心",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} は回して取り付けられているため、ドラッグでサイズを変えられません。",
847
+ "figure.text.gizmo-size-tilted": "{part} は傾いているため、ドラッグでサイズを変えられません。",
848
+ "figure.button.to-size-fields": "サイズ欄へ",
850
849
  "figure.label.align-input": "位置欄に従う ({field})",
851
850
  "figure.label.tab-short-properties": "属性",
852
851
  "figure.label.tab-short-capability": "能力",
@@ -1207,7 +1206,6 @@
1207
1206
  "figure.label.public": "公開",
1208
1207
  "figure.button.make-public": "公開する",
1209
1208
  "figure.button.make-private": "公開をやめる",
1210
- "figure.button.public-figures": "他ドメインの公開 Figure",
1211
1209
  "figure.text.category-choose": "分類を選ぶ",
1212
1210
  "figure.text.property-section-failed": "{section} の区画を描けませんでした: {reason}"
1213
1211
  }
@@ -507,6 +507,7 @@
507
507
  "figure.text.what-a-figure-is": "Figure 는 직접 저작한 3D 컴포넌트입니다. 단순한 도형을 조합해 만들면 도면에 배치할 수 있는 컴포넌트가 되고, 여러 개를 배치해도 형상과 재질을 공유합니다.",
508
508
  "figure.text.what-changed-in-this-release": "이번 버전에서 바뀐 것",
509
509
  "figure.title.figures": "Figure 카탈로그",
510
+ "figure.title.public-figures": "공개 Figure",
510
511
  "figure.title.new-figure": "새 Figure",
511
512
  "figure.title.save-new-figure": "Figure 초안 저장",
512
513
  "figure.text.no-parts-yet": "아직 부품이 없습니다. 위에서 도형을 선택하여 놓으세요.",
@@ -842,11 +843,9 @@
842
843
  "figure.text.held-on-part": "{other}의 {otherFace}에 {face}으로 붙어 있음",
843
844
  "figure.button.open-fastening": "붙임 설정 열기",
844
845
  "figure.button.detach-held": "붙임 풀기",
845
- "figure.text.gizmo-size-turned-fastening": "{part} 부품은 회전하거나 비틀린 상태로 부착되어 드래그 중 표시되는 면과 최종 적용되는 면이 다릅니다. 크기 항목에 숫자를 직접 입력해 주세요. 배치 기준면(또는 {extreme})의 위치는 유지됩니다.",
846
- "figure.label.kept-extreme-floor": "가장 낮은 점",
847
- "figure.label.kept-extreme-ceiling": "가장 높은 점",
848
- "figure.label.kept-extreme-wall": "가장 뒤쪽 점",
849
- "figure.label.kept-extreme-center": "중심",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} 부품은 돌려 붙어 있어 끌어서 크기를 바꿀 수 없습니다.",
847
+ "figure.text.gizmo-size-tilted": "{part} 부품은 기울어 있어 끌어서 크기를 바꿀 수 없습니다.",
848
+ "figure.button.to-size-fields": "크기 칸으로",
850
849
  "figure.label.align-input": "위치 칸을 따름 ({field})",
851
850
  "figure.label.tab-short-properties": "속성",
852
851
  "figure.label.tab-short-capability": "능력",
@@ -1207,7 +1206,6 @@
1207
1206
  "figure.label.public": "공개",
1208
1207
  "figure.button.make-public": "공개하기",
1209
1208
  "figure.button.make-private": "공개 끄기",
1210
- "figure.button.public-figures": "다른 도메인의 공개 Figure",
1211
1209
  "figure.text.category-choose": "분류 고르기",
1212
1210
  "figure.text.property-section-failed": "{section} 구역을 그리지 못했습니다: {reason}"
1213
1211
  }
@@ -507,6 +507,7 @@
507
507
  "figure.text.what-a-figure-is": "a Figure is a 3D component you author. Compose it from simple shapes and it becomes a component you can place on a drawing, sharing shape and material across every instance.",
508
508
  "figure.text.what-changed-in-this-release": "apa yang berubah dalam terbitan ini",
509
509
  "figure.title.figures": "katalog figure",
510
+ "figure.title.public-figures": "figure awam",
510
511
  "figure.title.new-figure": "new figure",
511
512
  "figure.title.save-new-figure": "simpan draf figure",
512
513
  "figure.text.no-parts-yet": "No parts yet. Pick a shape above to place one.",
@@ -842,11 +843,9 @@
842
843
  "figure.text.held-on-part": "Dilekatkan dengan {face} pada {otherFace} {other}",
843
844
  "figure.button.open-fastening": "Buka tetapan lekatan",
844
845
  "figure.button.detach-held": "Tanggalkan",
845
- "figure.text.gizmo-size-turned-fastening": "{part} diputar condong atau dilekatkan dipulas, jadi muka yang dikekalkan semasa diseret dan semasa ditulis akan berbeza. Taip saiz dalam medannya -- muka letakan (atau {extreme}) kekal di tempatnya.",
846
- "figure.label.kept-extreme-floor": "titik terendah",
847
- "figure.label.kept-extreme-ceiling": "titik tertinggi",
848
- "figure.label.kept-extreme-wall": "titik paling belakang",
849
- "figure.label.kept-extreme-center": "tengah",
846
+ "figure.text.gizmo-size-turned-fastening": "{part} dilekatkan dalam keadaan berpusing, jadi saiznya tidak boleh diseret.",
847
+ "figure.text.gizmo-size-tilted": "{part} condong, jadi saiznya tidak boleh diseret.",
848
+ "figure.button.to-size-fields": "Ke medan saiz",
850
849
  "figure.label.align-input": "Mengikut medannya ({field})",
851
850
  "figure.label.tab-short-properties": "Sifat",
852
851
  "figure.label.tab-short-capability": "Keupayaan",
@@ -1207,7 +1206,6 @@
1207
1206
  "figure.label.public": "awam",
1208
1207
  "figure.button.make-public": "jadikan awam",
1209
1208
  "figure.button.make-private": "jadikan peribadi",
1210
- "figure.button.public-figures": "figure awam domain lain",
1211
1209
  "figure.text.category-choose": "pilih kategori",
1212
1210
  "figure.text.property-section-failed": "bahagian {section} tidak dapat dilukis: {reason}"
1213
1211
  }