@things-factory/figure-ui 10.1.225 → 10.1.227

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 (94) hide show
  1. package/client/graphql/index.ts +6 -6
  2. package/client/modeller/consumer-capabilities.ts +15 -0
  3. package/client/modeller/editor-styles.ts +18 -1
  4. package/client/modeller/editor.ts +38 -26
  5. package/client/modeller/figure-capability-panel.ts +31 -10
  6. package/client/modeller/figure-clips.ts +2 -1
  7. package/client/modeller/figure-controls.ts +4 -3
  8. package/client/modeller/figure-data-panel.ts +24 -7
  9. package/client/modeller/figure-history-panel.ts +7 -7
  10. package/client/modeller/figure-links.ts +5 -5
  11. package/client/modeller/figure-material-panel.ts +2 -0
  12. package/client/modeller/figure-preview.ts +69 -2
  13. package/client/modeller/figure-recipe-catalog.ts +4 -0
  14. package/client/modeller/figure-slot-layout-panel.ts +25 -8
  15. package/client/modeller/figure-verdict.ts +4 -1
  16. package/client/modeller/unit-words.ts +11 -0
  17. package/client/pages/figure-list-page.ts +2 -2
  18. package/client/pages/figure-modeller-page.ts +7 -6
  19. package/client/types.ts +2 -2
  20. package/client/viewparts/figure-thumb.ts +1 -1
  21. package/dist-client/graphql/index.d.ts +5 -5
  22. package/dist-client/graphql/index.js +6 -6
  23. package/dist-client/graphql/index.js.map +1 -1
  24. package/dist-client/modeller/consumer-capabilities.d.ts +11 -0
  25. package/dist-client/modeller/consumer-capabilities.js +14 -0
  26. package/dist-client/modeller/consumer-capabilities.js.map +1 -1
  27. package/dist-client/modeller/editor-styles.js +18 -1
  28. package/dist-client/modeller/editor-styles.js.map +1 -1
  29. package/dist-client/modeller/editor.js +38 -26
  30. package/dist-client/modeller/editor.js.map +1 -1
  31. package/dist-client/modeller/figure-capability-panel.d.ts +3 -0
  32. package/dist-client/modeller/figure-capability-panel.js +29 -11
  33. package/dist-client/modeller/figure-capability-panel.js.map +1 -1
  34. package/dist-client/modeller/figure-clips.js +2 -1
  35. package/dist-client/modeller/figure-clips.js.map +1 -1
  36. package/dist-client/modeller/figure-controls.js +4 -3
  37. package/dist-client/modeller/figure-controls.js.map +1 -1
  38. package/dist-client/modeller/figure-data-panel.js +24 -7
  39. package/dist-client/modeller/figure-data-panel.js.map +1 -1
  40. package/dist-client/modeller/figure-history-panel.d.ts +5 -5
  41. package/dist-client/modeller/figure-history-panel.js +7 -7
  42. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  43. package/dist-client/modeller/figure-links.js +5 -5
  44. package/dist-client/modeller/figure-links.js.map +1 -1
  45. package/dist-client/modeller/figure-material-panel.js +2 -0
  46. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  47. package/dist-client/modeller/figure-preview.d.ts +3 -0
  48. package/dist-client/modeller/figure-preview.js +78 -2
  49. package/dist-client/modeller/figure-preview.js.map +1 -1
  50. package/dist-client/modeller/figure-recipe-catalog.js +4 -0
  51. package/dist-client/modeller/figure-recipe-catalog.js.map +1 -1
  52. package/dist-client/modeller/figure-slot-layout-panel.d.ts +1 -1
  53. package/dist-client/modeller/figure-slot-layout-panel.js +25 -8
  54. package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
  55. package/dist-client/modeller/figure-verdict.js +4 -1
  56. package/dist-client/modeller/figure-verdict.js.map +1 -1
  57. package/dist-client/modeller/unit-words.d.ts +5 -0
  58. package/dist-client/modeller/unit-words.js +12 -0
  59. package/dist-client/modeller/unit-words.js.map +1 -0
  60. package/dist-client/pages/figure-list-page.d.ts +1 -1
  61. package/dist-client/pages/figure-list-page.js +2 -2
  62. package/dist-client/pages/figure-list-page.js.map +1 -1
  63. package/dist-client/pages/figure-modeller-page.d.ts +3 -3
  64. package/dist-client/pages/figure-modeller-page.js +7 -6
  65. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  66. package/dist-client/tsconfig.tsbuildinfo +1 -1
  67. package/dist-client/types.d.ts +2 -2
  68. package/dist-client/types.js.map +1 -1
  69. package/dist-client/viewparts/figure-thumb.js +1 -1
  70. package/dist-client/viewparts/figure-thumb.js.map +1 -1
  71. package/package.json +9 -9
  72. package/test/browser/detail-page.js +1 -1
  73. package/test/browser/editing-page.js +3 -3
  74. package/test/browser/editing.check.mjs +5 -5
  75. package/test/browser/links-page.js +1 -1
  76. package/test/browser/links.check.mjs +2 -3
  77. package/test/browser/modeller-page.js +4 -4
  78. package/test/browser/modeller-revision.check.mjs +5 -5
  79. package/test/browser/modeller.check.mjs +5 -5
  80. package/test/browser/parts.check.mjs +2 -2
  81. package/test/browser/product-page.check.mjs +6 -0
  82. package/test/browser/props.check.mjs +3 -4
  83. package/test/browser/row-preview.check.mjs +3 -3
  84. package/test/browser/screen.check.mjs +13 -11
  85. package/test/consumer-capabilities.test.ts +7 -0
  86. package/test/links.test.ts +1 -1
  87. package/test/refusals.test.ts +1 -1
  88. package/test/resolve-ts-siblings.mjs +3 -3
  89. package/translations/en.json +23 -12
  90. package/translations/es.json +23 -12
  91. package/translations/ja.json +24 -13
  92. package/translations/ko.json +83 -72
  93. package/translations/ms.json +23 -12
  94. package/translations/zh.json +23 -12
@@ -44,9 +44,9 @@ export interface Figure {
44
44
  } | null;
45
45
  }
46
46
  /**
47
- * 발행된 판 하나.
47
+ * 발행된 버전 하나.
48
48
  *
49
- * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.
49
+ * 저작 중 저장은 여기 오지 않는다 — 버전은 발행에서만 생긴다.
50
50
  */
51
51
  export interface FigureVersion {
52
52
  version: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * 화면이 다루는 모양. 피규어 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.\n */\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 상세 또는 저작면을 열 때만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n /** Namespace of the full name {namespace}/{type} (ADR-0114). */\n namespace?: string | null\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** 피규어 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n /** Retired: still released and served, no longer offered for new use. */\n deprecated?: boolean\n /** The type to use instead of a retired figure, in the same domain. */\n supersededBy?: string | null\n /** Public: other domains read its releases and draw it by full name (LV figure-server design G2). */\n public?: boolean\n /** The domain it belongs to; carried by another domain's public figure, so a screen says whose it is. */\n domain?: { id: string; name?: string; subdomain?: string } | null\n}\n\n/**\n * 발행된 판 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 판본은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */\n type?: string\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../client/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * 화면이 다루는 모양. 피규어 자산의 타입은 `@hatiolab/figure-model` 이 정한다 — 여기서 다시 적지 않는다.\n */\n\n/** 서버가 돌려주는 Figure 한 벌. `source` 는 상세 또는 저작면을 열 때만 받는다. */\nexport interface Figure {\n id: string\n /** 컴포넌트 타입 이름. **만든 뒤에 고칠 수 없다.** */\n type: string\n /** Namespace of the full name {namespace}/{type} (ADR-0114). */\n namespace?: string | null\n name: string\n description?: string\n /** 분류 하나 — 팔레트에서 묶어 보이는 기준. */\n category?: string\n /** 태그 여럿 — 분류로는 못 하는 찾기. */\n tags?: string[]\n state?: 'draft' | 'released'\n /** 재활용 점수 0~100. 서버가 정본에서 계산한다 — 화면이 보내는 값이 아니다. */\n score?: number\n triangles?: number\n groups?: number\n thumbnail?: string\n thumbnailUpdatedAt?: string\n updatedAt?: string\n updater?: { id: string; name: string }\n /** 피규어 자산 JSON 문자열. 목록 질의에는 실려 오지 않는다. */\n source?: string\n properties?: string\n version?: number\n /** Retired: still released and served, no longer offered for new use. */\n deprecated?: boolean\n /** The type to use instead of a retired figure, in the same domain. */\n supersededBy?: string | null\n /** Public: other domains read its releases and draw it by full name (LV figure-server design G2). */\n public?: boolean\n /** The domain it belongs to; carried by another domain's public figure, so a screen says whose it is. */\n domain?: { id: string; name?: string; subdomain?: string } | null\n}\n\n/**\n * 발행된 버전 하나.\n *\n * 저작 중 저장은 여기 오지 않는다 — 버전은 발행에서만 생긴다.\n */\nexport interface FigureVersion {\n version: number\n /** 발행하며 남긴 말. */\n comment?: string\n state?: 'draft' | 'released'\n updatedAt?: string\n updater?: { id: string; name: string }\n score?: number\n triangles?: number\n groups?: number\n}\n\nexport interface FigureListResult {\n items: Figure[]\n total: number\n}\n\n/**\n * 새 Figure.\n *\n * `score`·`triangles`·`groups` 가 없다 — **서버가 정본에서 계산한다.** 화면이 보내면\n * 거짓이 들어오고 목록이 실물과 다른 것을 보여 준다.\n */\nexport interface NewFigure {\n type: string\n name: string\n description?: string\n category?: string\n tags?: string[]\n source: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/**\n * 수정.\n *\n * `type` 이 없다 — 저장되는 식별자라 고칠 수 없다. 배치된 도면이 그 이름으로 타입을\n * 찾으므로, 바꾸면 그 도면이 빈 화면이 된다.\n */\nexport interface FigurePatch {\n /** 발행 전 초안에서만 받는다 — 발행된 뒤에는 문이 거절한다(ADR-0063 결정 4). */\n type?: string\n name?: string\n description?: string\n category?: string\n /** 빈 배열은 「전부 지운다」, 안 주는 것은 「건드리지 않는다」. */\n tags?: string[]\n source?: string\n properties?: string\n state?: 'draft' | 'released'\n thumbnail?: string\n}\n\n/** 검증 결과 한 줄. 코드는 figure-model 의 것을 그대로 쓴다. */\nexport interface FigureFinding {\n code: string\n message: string\n at?: string\n}\n\n/**\n * 발행 판정 한 줄.\n *\n * `FigureFinding` 과 달리 **`blocking` 이 있다.** 저장 쪽 발견은 정의상 막지 않는 것들이고,\n * 발행 판정은 막는 것과 알리는 것을 함께 내므로 어느 쪽인지를 값이 스스로 말한다 — 코드를 보고\n * 화면이 다시 분류하면 규칙이 두 벌이 된다.\n */\nexport interface FigureGateFinding {\n code: string\n message: string\n /** 왜 그것이 문제인가 — 사람이 무엇을 보게 되나. */\n why?: string\n /** 무엇을 바꾸면 되나 — 부품 이름과 값으로. */\n how?: string\n /** 갈라진 부품 이름들, 또는 정본 안의 경로. */\n at?: string\n blocking: boolean\n}\n\n/** 발행해도 되나. `blocked` 면 서버가 거절한다. */\nexport interface FigureInspection {\n blocked: boolean\n findings: FigureGateFinding[]\n}\n"]}
@@ -11,7 +11,7 @@ import { hasBeenReleased } from '../figure-released.js';
11
11
  * 그림을 부르는 주소.
12
12
  *
13
13
  * **base64 를 목록 응답에 실어 오지 않는다.** data URL 은 브라우저가 캐시할 수 없어, 카탈로그를
14
- * 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 판을
14
+ * 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 버전을
15
15
  * 가리는 `v` 가 바뀐 것만 다시 받는다. 내주는 자리는 `figure-thumbnail-router` 다.
16
16
  *
17
17
  * 도메인 조각(`/domain/{subdomain}`)은 셸이 쓰는 것과 같은 함수로 읽는다 — 화면이 주소를
@@ -1 +1 @@
1
- {"version":3,"file":"figure-thumb.js","sourceRoot":"","sources":["../../client/viewparts/figure-thumb.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport '@operato/data-grist/ox-card-thumb.js'\nimport '@things-factory/figure-viewer/dist-client/figure-thumb-mark.js'\nimport { getPathInfo } from '@operato/utils'\n\nimport type { Figure } from '../types.js'\nimport { hasBeenReleased } from '../figure-released.js'\n\n/**\n * 그림을 부르는 주소.\n *\n * **base64 를 목록 응답에 실어 오지 않는다.** data URL 은 브라우저가 캐시할 수 없어, 카탈로그를\n * 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 판을\n * 가리는 `v` 가 바뀐 것만 다시 받는다. 내주는 자리는 `figure-thumbnail-router` 다.\n *\n * 도메인 조각(`/domain/{subdomain}`)은 셸이 쓰는 것과 같은 함수로 읽는다 — 화면이 주소를\n * 손으로 짜맞추면 도메인이 바뀌는 배치에서 조용히 틀린다.\n */\nfunction thumbnailSrc(figure: Figure): string | undefined {\n if (!figure.thumbnailUpdatedAt) {\n return undefined\n }\n\n const { contextPath } = getPathInfo(location.pathname)\n const stamp = new Date(figure.thumbnailUpdatedAt).getTime()\n\n return `${contextPath}/figure-thumbnail/${figure.id}?v=${stamp}`\n}\n\n/**\n * 카드의 그림 칸. 그림 · 그림 없음 · 초안 표시는 모든 카드 카탈로그의 `ox-card-thumb`(@operato/data-grist)가\n * 그리고, 여기서는 그 위에 이 카탈로그의 것(태그 · 삭제)만 얹는다(ADR-0117 개정 2026-10-13).\n *\n * ## 왜 컴포넌트인가\n *\n * 카탈로그는 `ox-grist` 의 CARD 모드로 그린다. 격자에게 그림 칸은 컬럼 하나이고, 그 칸을 그리는\n * 것은 컬럼의 `record.renderer` 다. 그림 위에 얹히는 것들(초안 표시, 삭제)은 **그림 칸 안에**\n * 있어야 하므로 renderer 가 그려야 하는데, renderer 의 결과는 `ox-card-field` 의 shadow root\n * 안으로 들어간다 — 화면의 `static styles` 가 닿지 않고, 인라인 style 로는 `:hover` 를 쓸 수 없다.\n *\n * 그래서 그림 칸만 컴포넌트로 둔다. 자기 스타일을 갖고 있으니 hover 도 테마도 정상으로 돈다.\n * 격자로 옮기기 전 손으로 만들었던 카드의 위쪽 절반이 그대로 이 자리다.\n *\n * ## 삭제가 왜 여기 있나\n *\n * 격자의 gutter 줄에 두면 카드 아래에 단추 줄이 하나 생긴다. 감춰도 자리가 남고, 자리를 없애면\n * 눌릴 곳이 사라진다. 되돌릴 수 없는 조작은 **고르는 눈길에 걸리지 않아야** 하므로 그림 오른쪽\n * 위, 손이 갔을 때만 나오는 자리에 둔다.\n */\n@customElement('figure-thumb')\nexport class FigureThumb extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n /*\n 태그 — **그림 위에 얹는다.**\n\n 카드 본문에 한 줄로 두었더니 태그가 있는 카드만 그만큼 높아져 격자가 들쭉날쭉해졌다.\n 태그는 피규어마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가\n 잡혀 있고(초안 표시·삭제), 얹어도 카드 높이가 변하지 않는다.\n\n 아래쪽에 둔다 — 위는 초안 표시와 삭제 단추가 쓴다.\n\n **투명한 알약이다.** 면을 꽉 채우면 그림 위에 흰 띠가 생겨 형상을 가린다 — 카탈로그에서\n 먼저 보아야 하는 것은 형상이다. 그래서 바탕은 거의 비우고, 읽히는 것은 뒤를 흐리는 것과\n 실낱 테두리가 맡는다. 흐림이 안 되는 환경에서도 테두리가 남아 글자가 형상에 묻히지 않는다.\n */\n div[tags] {\n position: absolute;\n right: 6px;\n bottom: 6px;\n left: 6px;\n display: flex;\n flex-wrap: wrap-reverse;\n justify-content: flex-end;\n gap: 3px;\n /* 태그가 많아도 그림을 덮지 않는다 — 넘치는 것은 위로 잘린다 */\n max-height: 44px;\n overflow: hidden;\n /* 알약 사이의 빈 자리는 그림이 받는다 — 거기서 누르면 저작면이 열린다 */\n pointer-events: none;\n }\n /*\n **알약은 누를 수 있다.** 누르면 그 태그로 목록을 거른다.\n\n 태그를 눈에 보이게 한 다음의 물음은 「그래서 이걸로 뭘 하나」다. 같은 태그가 붙은 것을\n 보려고 태그를 눈으로 읽어 검색창에 옮겨 적게 두면, 보여 준 뜻이 절반만 산다.\n\n 카드 자체가 저작면을 여는 자리이므로 알약에서 그 클릭을 멈춘다 — 안 멈추면 태그를 누른\n 사람이 편집기로 끌려간다.\n */\n div[tags] button {\n pointer-events: auto;\n cursor: pointer;\n padding: 0 7px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n line-height: 1.7;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 34%, transparent);\n border: 1px solid color-mix(in srgb, var(--md-sys-color-outline, #888) 42%, transparent);\n backdrop-filter: blur(4px);\n border-radius: 999em;\n white-space: nowrap;\n }\n div[tags] button:hover,\n div[tags] button:focus-visible {\n color: var(--md-sys-color-on-surface);\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 72%, transparent);\n border-color: var(--md-sys-color-primary);\n outline: none;\n }\n\n button[remove] {\n position: absolute;\n top: 5px;\n right: 5px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n /* 그림이 밝을 수도 어두울 수도 있다 — 옅은 면을 깔아 아이콘이 어디서나 읽히게 한다 */\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 82%, transparent);\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s ease-out;\n }\n button[remove] md-icon {\n --md-icon-size: 18px;\n }\n :host(:hover) button[remove],\n button[remove]:focus-visible {\n opacity: 1;\n }\n button[remove]:hover {\n color: var(--md-sys-color-on-error, #fff);\n background-color: var(--md-sys-color-error);\n }\n `\n\n @property({ type: Object }) figure!: Figure\n /** Another domain's public figure (G2): its card says whose it is and offers no delete. */\n @property({ type: Boolean }) readonly = false\n\n render() {\n const figure = this.figure\n if (!figure) {\n return html``\n }\n\n const src = thumbnailSrc(figure)\n\n return html`\n <ox-card-thumb\n src=${src || ''}\n alt=${figure.name || ''}\n badge=${this.readonly ? figure.domain?.name ?? '' : figure.deprecated ? i18next.t('figure.label.retired') : figure.state === 'released' ? '' : i18next.t('figure.label.draft')}\n >\n <figure-thumb-mark slot=\"empty\" .label=${i18next.t('figure.text.thumbnail-not-made-yet')}></figure-thumb-mark>\n ${figure.tags?.length\n ? html`<div tags>\n ${figure.tags.map(\n tag => html`\n <button\n title=${i18next.t('figure.text.find-by-this-tag')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.dispatchEvent(\n new CustomEvent('search-tag', { detail: { tag }, bubbles: true, composed: true })\n )\n }}\n >\n ${tag}\n </button>\n `\n )}\n </div>`\n : ''}\n\n ${this.readonly || hasBeenReleased(figure) ? '' : html`<button\n remove\n title=${i18next.t('figure.button.delete-figure')}\n @click=${(e: Event) => {\n /* 카드 클릭(=저작면 열기)까지 올라가지 않게 여기서 멈춘다 */\n e.stopPropagation()\n this.dispatchEvent(new CustomEvent('remove'))\n }}\n >\n <md-icon>delete</md-icon>\n </button>`}\n </ox-card-thumb>\n `\n }\n}\n"]}
1
+ {"version":3,"file":"figure-thumb.js","sourceRoot":"","sources":["../../client/viewparts/figure-thumb.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport '@operato/data-grist/ox-card-thumb.js'\nimport '@things-factory/figure-viewer/dist-client/figure-thumb-mark.js'\nimport { getPathInfo } from '@operato/utils'\n\nimport type { Figure } from '../types.js'\nimport { hasBeenReleased } from '../figure-released.js'\n\n/**\n * 그림을 부르는 주소.\n *\n * **base64 를 목록 응답에 실어 오지 않는다.** data URL 은 브라우저가 캐시할 수 없어, 카탈로그를\n * 열 때마다 그림 전부가 다시 온다([확인함] 표본 14개 227KB). 주소로 부르면 캐시가 되고, 버전을\n * 가리는 `v` 가 바뀐 것만 다시 받는다. 내주는 자리는 `figure-thumbnail-router` 다.\n *\n * 도메인 조각(`/domain/{subdomain}`)은 셸이 쓰는 것과 같은 함수로 읽는다 — 화면이 주소를\n * 손으로 짜맞추면 도메인이 바뀌는 배치에서 조용히 틀린다.\n */\nfunction thumbnailSrc(figure: Figure): string | undefined {\n if (!figure.thumbnailUpdatedAt) {\n return undefined\n }\n\n const { contextPath } = getPathInfo(location.pathname)\n const stamp = new Date(figure.thumbnailUpdatedAt).getTime()\n\n return `${contextPath}/figure-thumbnail/${figure.id}?v=${stamp}`\n}\n\n/**\n * 카드의 그림 칸. 그림 · 그림 없음 · 초안 표시는 모든 카드 카탈로그의 `ox-card-thumb`(@operato/data-grist)가\n * 그리고, 여기서는 그 위에 이 카탈로그의 것(태그 · 삭제)만 얹는다(ADR-0117 개정 2026-10-13).\n *\n * ## 왜 컴포넌트인가\n *\n * 카탈로그는 `ox-grist` 의 CARD 모드로 그린다. 격자에게 그림 칸은 컬럼 하나이고, 그 칸을 그리는\n * 것은 컬럼의 `record.renderer` 다. 그림 위에 얹히는 것들(초안 표시, 삭제)은 **그림 칸 안에**\n * 있어야 하므로 renderer 가 그려야 하는데, renderer 의 결과는 `ox-card-field` 의 shadow root\n * 안으로 들어간다 — 화면의 `static styles` 가 닿지 않고, 인라인 style 로는 `:hover` 를 쓸 수 없다.\n *\n * 그래서 그림 칸만 컴포넌트로 둔다. 자기 스타일을 갖고 있으니 hover 도 테마도 정상으로 돈다.\n * 격자로 옮기기 전 손으로 만들었던 카드의 위쪽 절반이 그대로 이 자리다.\n *\n * ## 삭제가 왜 여기 있나\n *\n * 격자의 gutter 줄에 두면 카드 아래에 단추 줄이 하나 생긴다. 감춰도 자리가 남고, 자리를 없애면\n * 눌릴 곳이 사라진다. 되돌릴 수 없는 조작은 **고르는 눈길에 걸리지 않아야** 하므로 그림 오른쪽\n * 위, 손이 갔을 때만 나오는 자리에 둔다.\n */\n@customElement('figure-thumb')\nexport class FigureThumb extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n /*\n 태그 — **그림 위에 얹는다.**\n\n 카드 본문에 한 줄로 두었더니 태그가 있는 카드만 그만큼 높아져 격자가 들쭉날쭉해졌다.\n 태그는 피규어마다 개수가 다르므로 본문에 두는 한 이 문제가 남는다. 그림 위는 이미 자리가\n 잡혀 있고(초안 표시·삭제), 얹어도 카드 높이가 변하지 않는다.\n\n 아래쪽에 둔다 — 위는 초안 표시와 삭제 단추가 쓴다.\n\n **투명한 알약이다.** 면을 꽉 채우면 그림 위에 흰 띠가 생겨 형상을 가린다 — 카탈로그에서\n 먼저 보아야 하는 것은 형상이다. 그래서 바탕은 거의 비우고, 읽히는 것은 뒤를 흐리는 것과\n 실낱 테두리가 맡는다. 흐림이 안 되는 환경에서도 테두리가 남아 글자가 형상에 묻히지 않는다.\n */\n div[tags] {\n position: absolute;\n right: 6px;\n bottom: 6px;\n left: 6px;\n display: flex;\n flex-wrap: wrap-reverse;\n justify-content: flex-end;\n gap: 3px;\n /* 태그가 많아도 그림을 덮지 않는다 — 넘치는 것은 위로 잘린다 */\n max-height: 44px;\n overflow: hidden;\n /* 알약 사이의 빈 자리는 그림이 받는다 — 거기서 누르면 저작면이 열린다 */\n pointer-events: none;\n }\n /*\n **알약은 누를 수 있다.** 누르면 그 태그로 목록을 거른다.\n\n 태그를 눈에 보이게 한 다음의 물음은 「그래서 이걸로 뭘 하나」다. 같은 태그가 붙은 것을\n 보려고 태그를 눈으로 읽어 검색창에 옮겨 적게 두면, 보여 준 뜻이 절반만 산다.\n\n 카드 자체가 저작면을 여는 자리이므로 알약에서 그 클릭을 멈춘다 — 안 멈추면 태그를 누른\n 사람이 편집기로 끌려간다.\n */\n div[tags] button {\n pointer-events: auto;\n cursor: pointer;\n padding: 0 7px;\n font: var(--label-font, inherit);\n font-size: 0.66rem;\n line-height: 1.7;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 34%, transparent);\n border: 1px solid color-mix(in srgb, var(--md-sys-color-outline, #888) 42%, transparent);\n backdrop-filter: blur(4px);\n border-radius: 999em;\n white-space: nowrap;\n }\n div[tags] button:hover,\n div[tags] button:focus-visible {\n color: var(--md-sys-color-on-surface);\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 72%, transparent);\n border-color: var(--md-sys-color-primary);\n outline: none;\n }\n\n button[remove] {\n position: absolute;\n top: 5px;\n right: 5px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));\n /* 그림이 밝을 수도 어두울 수도 있다 — 옅은 면을 깔아 아이콘이 어디서나 읽히게 한다 */\n background-color: color-mix(in srgb, var(--md-sys-color-surface, #fff) 82%, transparent);\n border: none;\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s ease-out;\n }\n button[remove] md-icon {\n --md-icon-size: 18px;\n }\n :host(:hover) button[remove],\n button[remove]:focus-visible {\n opacity: 1;\n }\n button[remove]:hover {\n color: var(--md-sys-color-on-error, #fff);\n background-color: var(--md-sys-color-error);\n }\n `\n\n @property({ type: Object }) figure!: Figure\n /** Another domain's public figure (G2): its card says whose it is and offers no delete. */\n @property({ type: Boolean }) readonly = false\n\n render() {\n const figure = this.figure\n if (!figure) {\n return html``\n }\n\n const src = thumbnailSrc(figure)\n\n return html`\n <ox-card-thumb\n src=${src || ''}\n alt=${figure.name || ''}\n badge=${this.readonly ? figure.domain?.name ?? '' : figure.deprecated ? i18next.t('figure.label.retired') : figure.state === 'released' ? '' : i18next.t('figure.label.draft')}\n >\n <figure-thumb-mark slot=\"empty\" .label=${i18next.t('figure.text.thumbnail-not-made-yet')}></figure-thumb-mark>\n ${figure.tags?.length\n ? html`<div tags>\n ${figure.tags.map(\n tag => html`\n <button\n title=${i18next.t('figure.text.find-by-this-tag')}\n @click=${(e: Event) => {\n e.stopPropagation()\n this.dispatchEvent(\n new CustomEvent('search-tag', { detail: { tag }, bubbles: true, composed: true })\n )\n }}\n >\n ${tag}\n </button>\n `\n )}\n </div>`\n : ''}\n\n ${this.readonly || hasBeenReleased(figure) ? '' : html`<button\n remove\n title=${i18next.t('figure.button.delete-figure')}\n @click=${(e: Event) => {\n /* 카드 클릭(=저작면 열기)까지 올라가지 않게 여기서 멈춘다 */\n e.stopPropagation()\n this.dispatchEvent(new CustomEvent('remove'))\n }}\n >\n <md-icon>delete</md-icon>\n </button>`}\n </ox-card-thumb>\n `\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.225",
3
+ "version": "10.1.227",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,8 +27,8 @@
27
27
  "clean": "npm run clean:server && npm run clean:client"
28
28
  },
29
29
  "dependencies": {
30
- "@hatiolab/figure-model": "^0.12.0",
31
- "@hatiolab/things-scene": "^10.1.102",
30
+ "@hatiolab/figure-model": "^0.12.1",
31
+ "@hatiolab/things-scene": "^10.1.104",
32
32
  "@material/web": "^2.0.0",
33
33
  "@operato/data-grist": "^10.29.43",
34
34
  "@operato/graphql": "^10.29.43",
@@ -39,14 +39,14 @@
39
39
  "@operato/p13n": "^10.29.43",
40
40
  "@operato/popup": "^10.29.43",
41
41
  "@operato/shell": "^10.29.43",
42
- "@operato/slot-layout-editor": "^10.29.57",
42
+ "@operato/slot-layout-editor": "^10.29.58",
43
43
  "@operato/styles": "^10.0.0",
44
44
  "@operato/utils": "^10.29.43",
45
- "@things-factory/ai-assistant": "^10.1.223",
46
- "@things-factory/figure-service": "^10.1.224",
47
- "@things-factory/figure-viewer": "^10.1.224",
48
- "@things-factory/setting-base": "^10.1.223",
45
+ "@things-factory/ai-assistant": "^10.1.227",
46
+ "@things-factory/figure-service": "^10.1.227",
47
+ "@things-factory/figure-viewer": "^10.1.227",
48
+ "@things-factory/setting-base": "^10.1.227",
49
49
  "three": "^0.185.1"
50
50
  },
51
- "gitHead": "9929ca76f614ff84bea1f61dabe0e1b7f00cd18a"
51
+ "gitHead": "f66591e3864aab08a6fb93fb1fcf1c9de6b546ac"
52
52
  }
@@ -26,7 +26,7 @@ async function start() {
26
26
  const kept = document.createElement('output'); kept.setAttribute('aria-label','썸네일')
27
27
  dock.keepThumbnail = async (id, url) => { kept.textContent = `${id} ${url.slice(0, 15)} ${url.length}` }
28
28
  const controls = document.createElement('nav')
29
- for (const [label, id] of [['클립 모델 열기','motion'],['연결 오류 시험','retry'],['깨진 발행판 시험','invalid'],['느린 모델 열기','slow']]) {
29
+ for (const [label, id] of [['클립 모델 열기','motion'],['연결 오류 시험','retry'],['깨진 발행 버전 시험','invalid'],['느린 모델 열기','slow']]) {
30
30
  const button = document.createElement('button'); button.textContent = label
31
31
  button.onclick = () => void dock.show(id); controls.append(button)
32
32
  }
@@ -96,7 +96,7 @@ window.editing = {
96
96
  propertyInputs() {
97
97
  /* The panel shown: the properties, or the 「반응형」 tab a check pressed. */
98
98
  const panel = editor().renderRoot.querySelector('div[panel]:not([off])')
99
- return [...panel.querySelectorAll('input')].map(i => i.getAttribute('aria-label'))
99
+ return [...panel.querySelectorAll('input')].map(i => (i.dataset?.field ?? i.getAttribute('aria-label')))
100
100
  },
101
101
  /** The page point of the first element a selector finds -- anywhere on the page, or inside the `host` elements -- and what it says. */
102
102
  at(selector, host) {
@@ -126,13 +126,13 @@ window.editing = {
126
126
  },
127
127
  /** The page point of a field's label: the axis letter of a row's cell, or a single field's name. */
128
128
  fieldLabel(id) {
129
- const input = deep('input').find(i => i.getAttribute('aria-label') === id)
129
+ const input = deep('input').find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === id)
130
130
  const label = input?.closest('div[cell]')?.querySelector('span[axis]') ?? input?.previousElementSibling
131
131
  return label ? centreOf(label) : null
132
132
  },
133
133
  /** What a field shows now. */
134
134
  fieldValue(id) {
135
- return deep('input').find(i => i.getAttribute('aria-label') === id)?.value ?? null
135
+ return deep('input').find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === id)?.value ?? null
136
136
  },
137
137
  /** How a clip track's head is laid out: its name, the repeat choice and the remove button, as boxes on the page. */
138
138
  trackHead() {
@@ -314,11 +314,11 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
314
314
  })
315
315
 
316
316
  /*
317
- A round body's measures are the 「배치」 tab's size editor (2026-10-24): its across said as a diameter, written as the
317
+ A round body's measures are the 「크기」 tab's size editor (2026-10-24): its across said as a diameter, written as the
318
318
  radius it halves to. The editor writes on change; it draws nothing while a number is typed.
319
319
  */
320
320
  test('a diameter typed on the arrangement tab is written once when it is left, as the radius it halves to', async () => {
321
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
321
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
322
322
  const start = await revision()
323
323
  await type('drum X · figure.label.size-diameter figure.label.size-measure', '600')
324
324
  assert.equal(await revision(), start, 'nothing written yet')
@@ -332,7 +332,7 @@ describe('the material: colours by purpose, one step each, a glow drawn while dr
332
332
  })
333
333
 
334
334
  /*
335
- A box's size is the 「배치」 tab's size editor now (2026-10-24); the numbers in hand on the properties tab are its place,
335
+ A box's size is the 「크기」 tab's size editor now (2026-10-24); the numbers in hand on the properties tab are its place,
336
336
  in the position row.
337
337
  */
338
338
  describe('values in hand on the properties tab: drawn at once, written on change (user 2026-09-24)', () => {
@@ -374,7 +374,7 @@ describe('values in hand on the properties tab: drawn at once, written on change
374
374
  })
375
375
  })
376
376
 
377
- /* A size of the part's own on the 「배치」 tab's size editor is a value in hand as any field is (user 2026-09-24). */
377
+ /* A size of the part's own on the 「크기」 tab's size editor is a value in hand as any field is (user 2026-09-24). */
378
378
  describe('a size in hand on the arrangement tab: drawn while typed or dragged, written once', () => {
379
379
  const WIDTH = 'box X figure.label.size-measure'
380
380
  const wide = async () => Math.round((await look('box')).size[0])
@@ -382,7 +382,7 @@ describe('a size in hand on the arrangement tab: drawn while typed or dragged, w
382
382
  before(async () => {
383
383
  await mount('box')
384
384
  await pick('box')
385
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
385
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
386
386
  })
387
387
 
388
388
  test('a width typed is drawn while it is typed, and written once on Enter', async () => {
@@ -39,7 +39,7 @@ window.look = {
39
39
  all(selector, text) {
40
40
  return deep(selector)
41
41
  .filter(saying(text))
42
- .map(e => ({ text: words(e), disabled: !!e.disabled, open: !!e.open, value: e.value ?? null, label: e.getAttribute('aria-label'), readOnly: !!e.readOnly, spotlit: e.hasAttribute('spotlit') }))
42
+ .map(e => ({ text: words(e), disabled: !!e.disabled, open: !!e.open, value: e.value ?? null, label: e.dataset?.field ?? e.getAttribute('aria-label'), readOnly: !!e.readOnly, spotlit: e.hasAttribute('spotlit') }))
43
43
  },
44
44
  /** A face of the target box in the fastening drawing: whether it can be pressed now, and whether it is the one chosen. */
45
45
  face(f) {
@@ -88,9 +88,8 @@ async function pick(part) {
88
88
  await click(await read(id => probe.row(id), part), `the row of ${part}`)
89
89
  await until(id => probe.state().selected === id, [part], `${part} selected`)
90
90
  }
91
- /* A side tab by its short name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
92
- const tab = name =>
93
- read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name).then(point => click(point, `the ${name} tab`))
91
+ /* A side tab by its short name: one name whatever is picked (「크기」, sweep 2026-10-27 J2). */
92
+ const tab = name => read(n => probe.tab(`figure.label.tab-short-${n}`), name).then(point => click(point, `the ${name} tab`))
94
93
  /** A fastening the part has is said in one line; 「바꾸기」 opens the fastening window on it. */
95
94
  async function changeFastening() {
96
95
  await until(() => look.all('button[change-fastening]').length === 1, [], 'the fastening said in one line')
@@ -203,9 +203,9 @@ window.probe = {
203
203
  const l = Math.hypot(b.x - a.x, b.y - a.y) || 1
204
204
  return { x: (b.x - a.x) / l, y: (b.y - a.y) / l }
205
205
  },
206
- /** The page point of an input or checkbox by its aria-label. */
206
+ /** The page point of an input or checkbox by its field id (data-field) or its aria-label. */
207
207
  labelled(label) {
208
- const input = deep('input').find(i => i.getAttribute('aria-label') === label)
208
+ const input = deep('input').find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === label)
209
209
  return input ? centreOf(input) : null
210
210
  },
211
211
  /** The page point of a button by an attribute it carries. */
@@ -262,7 +262,7 @@ window.probe = {
262
262
  focused() {
263
263
  let at = document.activeElement
264
264
  while (at?.shadowRoot?.activeElement) at = at.shadowRoot.activeElement
265
- return at ? { tag: at.localName, label: at.getAttribute('aria-label') } : null
265
+ return at ? { tag: at.localName, label: at.dataset?.field ?? at.getAttribute('aria-label') } : null
266
266
  },
267
267
  /** The page point of an input by its aria-label, or of the part name field. */
268
268
  nameField() {
@@ -276,7 +276,7 @@ window.probe = {
276
276
  },
277
277
  /** Whether an input with this label is on the page. */
278
278
  hasInput(label) {
279
- return deep('input').some(i => i.getAttribute('aria-label') === label)
279
+ return deep('input').some(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === label)
280
280
  },
281
281
  /** The page point of a part's row in the parts list. */
282
282
  row(id) {
@@ -74,7 +74,7 @@ describe('work counted on the robot arm', () => {
74
74
 
75
75
  /*
76
76
  The tabs are drawn out of sight with the rest: the selected part's motion is evaluated at its two ends, and the size
77
- editor on 「배치」 asks once which of the parent's axes each of the part's own lies along, the figure at rest
77
+ editor on 「크기」 asks once which of the parent's axes each of the part's own lies along, the figure at rest
78
78
  (figureAxisPairsOf, ADR-0159 3) -- one evaluation for all three rows.
79
79
  */
80
80
  test('picking a part on the canvas compiles nothing; it evaluates the two ends of the part\'s motion and its axes at rest', async () => {
@@ -98,8 +98,8 @@ describe('work counted on the robot arm', () => {
98
98
  ends, and its axes paired at rest for the size editor once more on the new version.
99
99
  */
100
100
  test('a value typed into its field: the edit and its read, and nothing else while it is drawn', async () => {
101
- /* The part's measures are the 「배치」 tab's size editor (2026-10-24). */
102
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
101
+ /* The part's measures are the 「크기」 tab's size editor (2026-10-24). */
102
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
103
103
  await settled()
104
104
  const field = await read(() => {
105
105
  const deep = (root = document) => [...root.querySelectorAll('input[type=number]')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
@@ -189,8 +189,8 @@ describe('the edit canvas and the preview stay standing', () => {
189
189
  test('a value changed beside the preview is drawn inside the scene that stands', async () => {
190
190
  await click(await read(() => probe.row('box')))
191
191
  await sleep(300)
192
- /* The box's size is the 「배치」 tab's (2026-10-24). */
193
- await click(await read(() => probe.tab('figure.label.tab-arrangement')))
192
+ /* The box's size is the 「크기」 tab's (2026-10-24). */
193
+ await click(await read(() => probe.tab('figure.label.tab-short-response')))
194
194
  await sleep(300)
195
195
  const field = await read(() => probe.labelled('box Y figure.label.size-measure'))
196
196
  assert.ok(field, 'the height field')
@@ -54,10 +54,10 @@ async function click(point) {
54
54
  await sleep(400)
55
55
  }
56
56
  /*
57
- A side tab pressed, by its short name; how a part answers a size is the 「반응형」 tab's (user 2026-10-17), named
58
- 「배치」 (figure.label.tab-arrangement) while a part is chosen.
57
+ A side tab pressed, by its short name; how a part answers a size is the 「크기」 tab's (user 2026-10-17, one name
58
+ whatever is picked since the sweep of 2026-10-27, J2).
59
59
  */
60
- const tab = async name => click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name))
60
+ const tab = async name => click(await read(n => probe.tab(`figure.label.tab-short-${n}`), name))
61
61
 
62
62
  const KEYS = { translate: 'KeyW', rotate: 'KeyE', scale: 'KeyR' }
63
63
 
@@ -430,7 +430,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
430
430
  const field = id =>
431
431
  page().evaluateHandle(id => {
432
432
  const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
433
- return deep().find(i => i.getAttribute('aria-label') === id) ?? null
433
+ return deep().find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === id) ?? null
434
434
  }, id)
435
435
  const declared = () =>
436
436
  page().evaluate(() => {
@@ -474,7 +474,7 @@ describe('a figure made in the modeller is given its size from its parts (user 2
474
474
  const field = () =>
475
475
  page().evaluateHandle(() => {
476
476
  const deep = (root = document) => [...root.querySelectorAll('input')].concat(...[...root.querySelectorAll('*')].filter(e => e.shadowRoot).map(e => deep(e.shadowRoot)))
477
- return deep().find(i => i.getAttribute('aria-label') === 'size.x.max') ?? null
477
+ return deep().find(i => (i.dataset?.field ?? i.getAttribute('aria-label')) === 'size.x.max') ?? null
478
478
  })
479
479
  /* Selected where it is drawn, as a person does, then typed over. */
480
480
  const typeOver = async keys => {
@@ -76,9 +76,9 @@ async function press(...keys) {
76
76
  for (const key of keys.slice(0, -1).reverse()) await page().keyboard.up(key)
77
77
  await sleep(400)
78
78
  }
79
- /* A side tab by its short name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
79
+ /* A side tab by its short name: one name whatever is picked (「크기」, sweep 2026-10-27 J2). */
80
80
  async function tab(name) {
81
- await click(await read(n => probe.tab(`figure.label.tab-short-${n}`) ?? (n === 'response' ? probe.tab('figure.label.tab-arrangement') : null), name), `the ${name} tab`)
81
+ await click(await read(n => probe.tab(`figure.label.tab-short-${n}`), name), `the ${name} tab`)
82
82
  }
83
83
 
84
84
  describe('the parts list: each button does what it says, one step of the session each', () => {
@@ -189,6 +189,9 @@ describe('the first save of a new figure', () => {
189
189
  })
190
190
 
191
191
  test('the question asks the name first, focused, with the type written before', async () => {
192
+ /* A new figure saves once it has a part (sweep 2026-10-27 U24): the start's box. */
193
+ assert.equal(await read(() => product.deep('figure-editor >>> button[save]')[0].disabled), true, 'nothing to save before a part')
194
+ await press('figure-editor >>> button[start-shape="box"]', 'a box from the start')
192
195
  await press('button[save]', 'save')
193
196
  await until(() => product.deep('figure-first-save')[0]?.shadowRoot.querySelector('#type'), 'the first save asked')
194
197
  await until(() => { let a = document.activeElement; while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement; return a?.id === 'name' }, 'the name focused')
@@ -283,6 +286,8 @@ describe('opening another figure', () => {
283
286
  await read(() => product.deep('figure-editor')[0].updateComplete)
284
287
  assert.equal(await read(() => product.deep('figure-editor >>> section[empty-figure]').length), 0)
285
288
  assert.equal(await read(() => product.page().context.title), 'figure.label.figure')
289
+ /* Named by the address before it is read: the rail marks the catalogue, not "new figure" (sweep 2026-10-27 U30). */
290
+ assert.equal(await read(() => product.page().context.menu), 'figure-list')
286
291
  await read(() => window.letB())
287
292
  await until(() => product.page().shadowRoot.querySelector('label[type-field] input')?.value === 'type-b', 'b\'s type')
288
293
  })
@@ -603,6 +608,7 @@ describe('the page around the figure: the shell, the dock, the properties', () =
603
608
  assert.deepEqual(await read(() => assistant.surface.anchor()), { anchorType: 'figure', anchorId: creationId })
604
609
  assert.deepEqual(await read(() => { const { figureId, editorMode } = assistant.surface.hostContext(); return { figureId, editorMode } }), { figureId: creationId, editorMode: 'new' })
605
610
  assert.equal((await read(() => product.context())).menu, undefined)
611
+ await press('figure-editor >>> button[start-shape="box"]', 'a box from the start')
606
612
  await rename('Gate')
607
613
  await read(() => { assistant.forget(); product.forget() })
608
614
  await press('button[save]', 'save')
@@ -94,10 +94,9 @@ const groupsInOrder = () =>
94
94
  .map(([name]) => name)
95
95
  })
96
96
 
97
- /* A side tab pressed, by its name; with a part chosen, 「반응형」 is named 「배치」 (figure.label.tab-arrangement). */
97
+ /* A side tab pressed, by its name: one name whatever is picked (「크기」, sweep 2026-10-27 J2). */
98
98
  async function tab(name) {
99
- const named = `aside button[tab][aria-label="figure.label.tab-short-${name}"]`
100
- await press(name === 'response' ? `${named}, aside button[tab][aria-label="figure.label.tab-arrangement"]` : named, `the ${name} tab`)
99
+ await press(`aside button[tab][aria-label="figure.label.tab-short-${name}"]`, `the ${name} tab`)
101
100
  await read(() => document.querySelector('figure-editor').updateComplete)
102
101
  }
103
102
 
@@ -152,7 +151,7 @@ describe('the side: six tabs, the view and the releases from the bar', () => {
152
151
  describe('a part\'s groups in one order, whatever its state', () => {
153
152
  before(() => mount('rollers'))
154
153
 
155
- /* A box's size is the 「배치」 tab's (2026-10-24): it has no measures of its own shape left on the properties. */
154
+ /* A box's size is the 「크기」 tab's (2026-10-24): it has no measures of its own shape left on the properties. */
156
155
  test('a part fastened in a row: name, place, look, more; on 「반응형」 its size and its row', async () => {
157
156
  await pick('roller')
158
157
  await tab('properties')
@@ -60,10 +60,10 @@ describe('a row\'s preview follows its basis', () => {
60
60
  const at = await read(() => probe.row('bin'))
61
61
  await page().mouse.click(at.x, at.y)
62
62
  await until(() => probe.state().selected === 'bin', [], 'bin selected')
63
- /* The row is the 「반응형」 tab's (user 2026-10-17), named 「배치」 while a part is chosen (figure.label.tab-arrangement). */
64
- const tab = await read(() => probe.tab('figure.label.tab-arrangement'))
63
+ /* The row is the 「반응형」 tab's (user 2026-10-17), named 「크기」 (sweep 2026-10-27 J2). */
64
+ const tab = await read(() => probe.tab('figure.label.tab-short-response'))
65
65
  await page().mouse.click(tab.x, tab.y)
66
- await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-arrangement', [], 'the 배치 tab on')
66
+ await until(() => document.querySelector('figure-editor').shadowRoot.querySelector('button[tab][on]')?.getAttribute('aria-label') === 'figure.label.tab-short-response', [], 'the 크기 tab on')
67
67
  await click('details[row-fold] > summary', {}, 'the row\'s fold')
68
68
  })
69
69
 
@@ -82,8 +82,8 @@ const tabOn = () => read(() => screenCheck.editor().shadowRoot.querySelector('bu
82
82
  const pressBar = action => press(action, a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`), action)
83
83
  const pressTab = name => press(name, name => screenCheck.editor().shadowRoot.querySelector(`button[tab][aria-label="${name}"]`), name)
84
84
  const TAB = name => `figure.label.tab-short-${name}`
85
- /* 「반응형」 while a part is chosen: named by what it holds for the part, its size and arrangement (2026-10-24). */
86
- const ARRANGEMENT = 'figure.label.tab-arrangement'
85
+ /* The size tab, one name whatever is picked (「크기」, sweep 2026-10-27 J2; it was 「배치」 while a part was chosen). */
86
+ const ARRANGEMENT = 'figure.label.tab-short-response'
87
87
  /*
88
88
  The view settings or the releases, opened from the bar over the work (they were tabs once); left open if open. The
89
89
  releases open from 「발행」, the bar's one way to them (user 2026-10-18).
@@ -220,7 +220,9 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
220
220
  test('the bar: undo and redo with their keys, the status, save, and the left pane folded to give the canvas its width', async () => {
221
221
  assert.equal(await read(() => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').pane === screenCheck.editor().shadowRoot.querySelector('ox-three-pane')), true, 'the bar folds the editor\'s own frame')
222
222
  const titles = await read(() => ['undo', 'redo'].map(a => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar').shadowRoot.querySelector(`button[data-action=${a}]`).title))
223
- assert.ok(titles[0].endsWith('(Ctrl+Z)') && titles[1].endsWith('(Ctrl+Y)'), `the keys in the titles: ${titles}`)
223
+ /* The keys as this platform writes them (sweep 2026-10-27 U26): ⌘Z · ⇧⌘Z on a Mac, Ctrl+Z · Ctrl+Y elsewhere. */
224
+ const mac = await read(() => navigator.userAgent.includes('Mac'))
225
+ assert.ok(titles[0].endsWith(mac ? '(⌘Z)' : '(Ctrl+Z)') && titles[1].endsWith(mac ? '(⇧⌘Z)' : '(Ctrl+Y)'), `the keys in the titles: ${titles}`)
224
226
  /* Saved or not is the save button's: pressable with something to save, dimmed without -- no words beside it. */
225
227
  const status = () => read(() => screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar [role=status]')?.textContent.trim() ?? null)
226
228
  const saveOn = () => read(() => !screenCheck.editor().shadowRoot.querySelector('ox-canvas-bar button[save]').disabled)
@@ -228,7 +230,7 @@ describe('the frame: parts · canvas · one side of seven tabs, and the bar over
228
230
  assert.equal(await saveOn(), false, 'saved: the save button is dimmed')
229
231
 
230
232
  await pressRow('box')
231
- /* A part's size is the 「배치」 tab's (2026-10-24). */
233
+ /* A part's size is the 「크기」 tab's (2026-10-24). */
232
234
  await pressTab(ARRANGEMENT)
233
235
  const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'box Y figure.label.size-measure'))
234
236
  await height.click({ clickCount: 3 })
@@ -339,10 +341,10 @@ describe('a part chosen: each tab in its place, the figure-wide tabs give way, t
339
341
  await pressTab(TAB('properties'))
340
342
  let now = await holds()
341
343
  assert.ok(now.subject, 'the part\'s head')
342
- /* A box's size is the 「배치」 tab's, in its size editor (2026-10-24): the properties keep its place. */
344
+ /* A box's size is the 「크기」 tab's, in its size editor (2026-10-24): the properties keep its place. */
343
345
  assert.ok(now.links.includes('figure.label.group-place') && !now.links.includes('figure.label.group-size'), `the shape's links: ${now.links}`)
344
346
  assert.ok(now.material, 'its material')
345
- /* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」), named 「배치」 while a part is chosen. */
347
+ /* How it answers a size has a tab of its own (user 2026-10-17, 「반응형」), named 「크기」. */
346
348
  await pressTab(ARRANGEMENT)
347
349
  now = await holds()
348
350
  assert.ok(now.subject, 'the part\'s head')
@@ -732,7 +734,7 @@ describe('the camera follows a size typed far from the view (first 15 minutes, 2
732
734
  test('a width typed ten times larger fits the view again; a little wider leaves the camera where it is', async () => {
733
735
  await mount('box')
734
736
  await pressRow('box')
735
- /* A part's size is the 「배치」 tab's (2026-10-24). */
737
+ /* A part's size is the 「크기」 tab's (2026-10-24). */
736
738
  await pressTab(ARRANGEMENT)
737
739
  await read(() => {
738
740
  const canvas = screenCheck.deep('figure-canvas')[0]
@@ -806,7 +808,7 @@ describe('a part\'s size row: a rule\'s numbers only where a rule sets every sid
806
808
  })
807
809
 
808
810
  /*
809
- A box's size is the 「배치」 tab's size editor (2026-10-24), one row a side: made to follow on every side, each row
811
+ A box's size is the 「크기」 tab's size editor (2026-10-24), one row a side: made to follow on every side, each row
810
812
  shows the size the rule gives it, and the properties draw no size row.
811
813
  */
812
814
  test('a box made to follow the figure\'s size on every side shows the size the rule gives it', async () => {
@@ -848,7 +850,7 @@ describe('the side panel is found as it was left, per part', () => {
848
850
  before(() => mount('arm'))
849
851
 
850
852
  /*
851
- The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one gives the panel
853
+ The part's properties fit a tall window since its size moved to 「크기」 (2026-10-24): a shorter one gives the panel
852
854
  something to scroll.
853
855
  */
854
856
  test('scrolled and a fold opened on one part; another part opens at its top; back, the first is as it was left', async () => {
@@ -1106,7 +1108,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1106
1108
 
1107
1109
  test('an edit is judged as it stands on screen: the source the server is sent is the one edited', async () => {
1108
1110
  await pressRow('left')
1109
- /* A part's size is the 「배치」 tab's (2026-10-24). */
1111
+ /* A part's size is the 「크기」 tab's (2026-10-24). */
1110
1112
  await pressTab(ARRANGEMENT)
1111
1113
  const height = await element('the height field', () => screenCheck.deep('input').find(i => i.getAttribute('aria-label') === 'left Y figure.label.size-measure'))
1112
1114
  await height.click({ clickCount: 3 })
@@ -1148,7 +1150,7 @@ describe('the modeller page: the editor with the page\'s figure panel, verdict a
1148
1150
  }
1149
1151
  })
1150
1152
 
1151
- /* The part's properties fit a tall window since its size moved to 「배치」 (2026-10-24): a shorter one scrolls. */
1153
+ /* The part's properties fit a tall window since its size moved to 「크기」 (2026-10-24): a shorter one scrolls. */
1152
1154
  test('another figure opened starts its panels clean: a part of the same name opens at its top', async () => {
1153
1155
  const viewport = page().viewport()
1154
1156
  await page().setViewport({ ...viewport, height: 600 })
@@ -14,6 +14,7 @@ import {
14
14
  inputTableAt,
15
15
  namesWith,
16
16
  needsMissing,
17
+ needsOffered,
17
18
  partsAt,
18
19
  settingsIssues,
19
20
  settingsOf,
@@ -58,6 +59,12 @@ test('what a capability still needs: the item mark, a name; put on and off keeps
58
59
  assert.deepEqual(needsMissing(CARRIER, asset), ['item'])
59
60
  assert.deepEqual(needsMissing(CARRIER, { ...asset, item: true }), [])
60
61
  assert.deepEqual(needsMissing({ ...(CARRIER as object), needs: ['Holdable', ['Capacity', 'FlowNode']] } as never, asset), ['Holdable', 'Capacity | FlowNode'])
62
+ /* Offered as the tab offers it (sweep 2026-10-27 U19): the item tag, and the rungs that give what is needed -- never its names. */
63
+ assert.deepEqual(needsOffered(CARRIER, asset), { item: true, rungs: [], names: [] })
64
+ const holder = { ...(CARRIER as object), needs: [['Holdable', 'FlowNode'], ['Capacity', 'FlowNode']] } as never
65
+ assert.deepEqual(needsOffered(holder, asset), { item: false, rungs: ['hold', 'flow'], names: [] })
66
+ assert.deepEqual(needsOffered({ ...(CARRIER as object), needs: ['FlowNode'] } as never, asset), { item: false, rungs: ['flow'], names: [] })
67
+ assert.deepEqual(needsOffered({ ...(CARRIER as object), needs: ['Nowhere'] } as never, asset), { item: false, rungs: [], names: ['Nowhere'] })
61
68
  const held = editFigurePart(asset, { kind: 'set-capabilities', names: ['Capacity', 'Holdable'] })
62
69
  assert.deepEqual(namesWith(held, 'acme.Carrier', true), ['Capacity', 'Holdable', 'acme.Carrier'])
63
70
  assert.deepEqual(namesWith(editFigurePart(held, { kind: 'set-capabilities', names: namesWith(held, 'acme.Carrier', true) }), 'acme.Carrier', false), ['Capacity', 'Holdable'])
@@ -630,7 +630,7 @@ test('팔레트의 통은 통의 크기로 서고, 세 축 모두 크기를 따
630
630
  assert.deepEqual(followableAxes(asset, 'tote'), ['x', 'y', 'z'])
631
631
  assert.deepEqual(gizmoReachOf(asset as never, 'tote').sizes, ['tote.dimension.width', 'tote.dimension.height', 'tote.dimension.depth'])
632
632
  const ko = wordsOf('ko')
633
- assert.equal(ko['figure.label.face-inside-bottom'], '안쪽 바닥')
633
+ assert.equal(ko['figure.label.face-inside-bottom'], '안쪽 바닥면')
634
634
  })
635
635
 
636
636
  /*
@@ -205,7 +205,7 @@ test('일상에서 코드로만 보이던 거절 넷 — 줄이 안 들어감 ·
205
205
  layout: { tiers: [{ tierNo: 1, anchorId: 'bin', mount: 'ON', position: { x: 0, y: 0, z: 0 }, rotation: 0, layout: { pattern: 'SINGLE', slot: { allowed: { width: 100, depth: 100, height: 100 }, accepts: ['TOTE'] } } }] }
206
206
  })
207
207
  const held = editRefusalOf(refusalOf(() => act(slotted, { kind: 'make-repeat', part: 'bin', axis: 'x', pitch: 150 })), say)
208
- assert.ok(held.includes('bin') && held.includes('적재 위치 배치') && !held.includes('SURFACE_PROVENANCE'), held)
208
+ assert.ok(held.includes('bin') && held.includes('자리 배치') && !held.includes('SURFACE_PROVENANCE'), held)
209
209
  /* A new part named as the row's part is (its copies drawn under ids of their own): the same words as a rename. */
210
210
  assert.match(editRefusalOf(refusalOf(() => fm.addFigureBox(conveyor(), { id: 'roller', size: { x: 1, y: 1, z: 1 }, position: { x: 0, y: 0, z: 0 } })), keyed), /part-name-taken .*"name":"roller"/)
211
211
  })
@@ -49,10 +49,10 @@ function tsSibling(specifier, parentURL, stripExtension) {
49
49
 
50
50
  /*
51
51
  * ── 패키지 이름 하나만 예외다: `@hatiolab/things-scene` (2026-09-24) ─────────────────────────
52
- * 이 패키지의 client 는 브라우저 코드이고, things-scene 은 판이 둘이다 — 브라우저용 `things-scene.mjs` 와
53
- * 서버 전송용 `things-scene-transfer.mjs`. Node 는 `exports` 의 `node` 조건으로 전송 판을 고르는데, 그 판에는
52
+ * 이 패키지의 client 는 브라우저 코드이고, things-scene 은 버전이 둘이다 — 브라우저용 `things-scene.mjs` 와
53
+ * 서버 전송용 `things-scene-transfer.mjs`. Node 는 `exports` 의 `node` 조건으로 전송 버전을 고르는데, 그 버전에는
54
54
  * figure 가 없다(`buildFigureGeometry` · `registerFigure` …). 모델러 소스를 Node 에서 시험하면 **브라우저가
55
- * 받을 판**을 봐야 맞다. 그래서 이 이름만 브라우저 판으로 되짚는다. 다른 패키지 이름은 그대로 둔다.
55
+ * 받을 버전**을 봐야 맞다. 그래서 이 이름만 브라우저 버전으로 되짚는다. 다른 패키지 이름은 그대로 둔다.
56
56
  */
57
57
  const SCENE_BROWSER_BUNDLE = (() => {
58
58
  try {