@things-factory/figure-ui 10.1.158 → 10.1.159

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-ui",
3
- "version": "10.1.158",
3
+ "version": "10.1.159",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -41,10 +41,10 @@
41
41
  "@operato/slot-layout-editor": "^10.29.25",
42
42
  "@operato/styles": "^10.0.0",
43
43
  "@operato/utils": "^10.0.0",
44
- "@things-factory/ai-assistant": "^10.1.157",
45
- "@things-factory/figure-service": "^10.1.157",
44
+ "@things-factory/ai-assistant": "^10.1.159",
45
+ "@things-factory/figure-service": "^10.1.159",
46
46
  "@things-factory/figure-viewer": "^10.1.155",
47
47
  "three": "^0.185.1"
48
48
  },
49
- "gitHead": "bbd9815106c199e136cfa30e33d9a27d821bf5a4"
49
+ "gitHead": "fc15cdcb18e562ca1e723a1e1bf042f9582cbbfb"
50
50
  }
@@ -25,13 +25,17 @@ const HERE = import.meta.dirname
25
25
  const FOLDER = 'harness-theme'
26
26
  const BRAND = join(HERE, '..', 'client', FOLDER, 'brand.css')
27
27
  const THEMES = ['light.css', 'dark.css'].map(f => join(HERE, '..', 'client', FOLDER, f))
28
- /** 저작 하네스 둘. 이 저장소 안의 두 번째 사본 자리다. */
29
- const HARNESSES = ['modeller-page.mjs', 'real-modeller.mjs'].map(f =>
28
+ /** 저작 하네스 — 앱의 모델러 페이지를 그대로 세우는 것 하나다(옛 V2 픽스처 페이지는 2026-10-13 에 지웠다). */
29
+ const HARNESSES = ['real-modeller.mjs'].map(f =>
30
30
  join(HERE, '..', '..', '..', 'test', 'browser', f)
31
31
  )
32
32
 
33
33
  const read = (path: string) => readFileSync(path, 'utf8')
34
34
 
35
+ /** brand.css 가 들여오는 teal — operato-suite 의 틀. 하네스의 `/pkg/` 처럼 저장소 안 경로로 읽는다. */
36
+ const TEAL_IMPORT = /@import url\('(\.\.\/\.\.\/\.\.\/operato-suite\/client\/frames\/teal\.css)'\);/
37
+ const TEAL = join(HERE, '..', 'client', FOLDER, '../../../operato-suite/client/frames/teal.css')
38
+
35
39
  /** `--이름: 값;` 을 모은다. 한 줄에 여러 개 붙어 있어도 잡는다. */
36
40
  function definitions(css: string): Map<string, string[]> {
37
41
  const found = new Map<string, string[]>()
@@ -69,6 +73,37 @@ function colorOf(css: string, name: string): string {
69
73
  return value
70
74
  }
71
75
 
76
+ /**
77
+ * 브랜드 이름의 실제 색. brand.css 는 `var(--teal-on-*)` 로 suite 를 가리키므로 그 값을 따라가
78
+ * 6자리 hex 로 돌려준다.
79
+ */
80
+ function brandColor(name: string): string {
81
+ const value = definitions(read(BRAND)).get(name)?.[0]
82
+ const teal = /^var\((--teal-on-(?:light|dark))\)$/.exec(value ?? '')
83
+
84
+ assert.ok(teal, `${name} 이 suite 의 teal 이름을 읽지 않는다: ${value}`)
85
+
86
+ const literal = definitions(read(TEAL)).get(teal[1]!)?.[0] ?? ''
87
+ const rgb = /^rgb\((\d+) (\d+) (\d+)\)$/.exec(literal)
88
+
89
+ assert.ok(rgb, `suite 의 ${teal[1]} 를 읽을 수 없다: ${literal}`)
90
+ return '#' + [rgb[1], rgb[2], rgb[3]].map(v => Number(v).toString(16).padStart(2, '0')).join('')
91
+ }
92
+
93
+ test('teal 값은 operato-suite 에만 있다 — brand.css 는 들여와 이름만 잇는다', () => {
94
+ /*
95
+ * operato-figure · operato-twin 의 앱 화면이 읽는 teal 과 하네스의 teal 이 사본으로 따로 살았다
96
+ * (2026-10-13 까지). 이제 brand.css 는 suite 의 `frames/teal.css` 를 들여오고 값을 적지 않는다.
97
+ */
98
+ const brand = read(BRAND)
99
+
100
+ assert.match(brand, TEAL_IMPORT, 'brand.css 가 suite 의 teal 틀을 들여오지 않는다')
101
+ assert.ok(existsSync(TEAL), `들여오는 자리에 teal 틀이 없다: ${TEAL}`)
102
+
103
+ const literals = [...brand.replace(/\/\*[\s\S]*?\*\//g, ' ').matchAll(/#[0-9a-f]{3,8}\b|rgba?\(/gi)].map(m => m[0])
104
+ assert.deepEqual(literals, [], `brand.css 가 색을 직접 적는다: ${literals.join(' · ')}`)
105
+ })
106
+
72
107
  test('브랜드 색의 정본이 있다 — 없으면 아래 테스트가 다 통과해 버린다', () => {
73
108
  const brand = definitions(read(BRAND))
74
109
 
@@ -168,7 +203,7 @@ test('산출물에 소스에 없는 css 가 없다 — copyfiles 는 지운 것
168
203
  옮긴 자리를 하네스가 가리키지 않으면 하네스는 스타일시트 없이 뜬다 — 오류 없이 무채색 화면이
169
204
  되고, 아래 대비 시험들은 **어느 화면에도 없는 값**을 재게 된다.
170
205
  */
171
- test('하네스 둘이 옮긴 팔레트를 가리킨다 — 밝은 것과 어두운 것 모두', () => {
206
+ test('하네스가 옮긴 팔레트를 가리킨다 — 밝은 것과 어두운 것 모두', () => {
172
207
  for (const path of HARNESSES) {
173
208
  const html = read(path)
174
209
 
@@ -189,14 +224,14 @@ test('밝은 면과 어두운 면을 함께 든다 — 한쪽만 있으면 어
189
224
  })
190
225
 
191
226
  test('브랜드 색 위의 흰 글자가 읽힌다 — 작은 글자 기준 4.5', () => {
192
- const figure = colorOf(read(BRAND), '--figure-brand')
227
+ const figure = brandColor('--figure-brand')
193
228
  const measured = contrast(figure, '#ffffff')
194
229
 
195
230
  assert.ok(measured >= 4.5, `${figure} 위 흰 글자 대비가 ${measured.toFixed(2)} 다`)
196
231
  })
197
232
 
198
233
  test('어두운 면 위의 브랜드 색이 읽힌다 — 그쪽은 -2 가 맡는다', () => {
199
- const bright = colorOf(read(BRAND), '--figure-brand-2')
234
+ const bright = brandColor('--figure-brand-2')
200
235
  const surface = colorOf(read(THEMES[1]!), '--md-sys-color-surface')
201
236
  const measured = contrast(bright, surface)
202
237
 
@@ -207,7 +242,7 @@ test('밝은 쪽 값을 어두운 면에 쓰지 않는다 — 이름이 뜻이
207
242
  /*
208
243
  * `--figure-brand` 를 어두운 면에 그대로 쓰면 대비 1.6 이라 사라진다. 그 실수를 잡는다.
209
244
  */
210
- const figure = colorOf(read(BRAND), '--figure-brand')
245
+ const figure = brandColor('--figure-brand')
211
246
  const darkSurface = colorOf(read(THEMES[1]!), '--md-sys-color-surface')
212
247
 
213
248
  assert.ok(
@@ -116,6 +116,9 @@ test('데이터 탭 · 클립 패널 · 미리보기의 배선과 문구', () =>
116
116
  assert.match(preview, /tellInstances\('motion'/)
117
117
  const clips = read('../client/modeller/figure-clips.ts')
118
118
  assert.match(clips, /kind: 'remove-driver', state: track\.state, clip: clip\.name/)
119
+ /* A key's value is shown to three decimals: a slider's 14.399999999999999 is not what a person typed. */
120
+ assert.match(clips, /value: current === undefined \? '' : shown\(current\)/)
121
+ assert.match(clips, /\$\{key\.seconds\}s → \$\{shown\(key\.value\)\}/)
119
122
  const keys = [read('../client/modeller/figure-data-panel.ts'), clips, read('../client/modeller/refusals.ts')].flatMap(src => [...src.matchAll(/'(figure\.[a-z.-]*[a-z])'/g)].map(m => m[1]!))
120
123
  /* Keys made from a value: each value the kernel can give has its words. */
121
124
  const made = [
@@ -0,0 +1,49 @@
1
+ import assert from 'node:assert/strict'
2
+ import { join } from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+ import test from 'node:test'
5
+
6
+ /**
7
+ * 저작 하네스(`test/browser/serve-modeller.mjs`)가 서고, 표본이 모델러에서 열린다.
8
+ *
9
+ * import map 이 꾸러미의 `exports` 하위 경로를 따른다.
10
+ *
11
+ * 하네스(`test/browser/serve.mjs`)는 `name/` 접두 하나만 두고 있었다. 그래서
12
+ * `@operato/popup/ox-popup-actions.js` 처럼 들이는 이름과 파일 자리(`dist/src/...`)가 다른 모듈이
13
+ * 패키지 맨 위에서 찾아져 404 가 되었고, 그것을 무는 모델러가 하네스에서 통째로 안 떴다(2026-10-13).
14
+ */
15
+
16
+ const SERVE = join(import.meta.dirname, '..', '..', '..', 'test', 'browser', 'serve.mjs')
17
+ const REAL = join(import.meta.dirname, '..', '..', '..', 'test', 'browser', 'real-modeller.mjs')
18
+
19
+ test('exports 의 하위 경로가 그 파일 자리로 간다', async () => {
20
+ const { buildImportMap } = await import(pathToFileURL(SERVE).href)
21
+ const { imports } = buildImportMap()
22
+
23
+ assert.equal(imports['@operato/popup/ox-popup-actions.js'], '/nm/@operato/popup/dist/src/ox-popup-actions.js')
24
+ assert.equal(imports['@operato/styles/action-heights.js'], '/nm/@operato/styles/dist/src/action-heights.js')
25
+ assert.equal(imports['@operato/popup/'], '/nm/@operato/popup/', 'the prefix stays for paths exports does not name')
26
+ })
27
+
28
+ test('표본은 모두 지금의 자산 형식이라 모델러가 연다', async () => {
29
+ /*
30
+ * 손으로 쓴 옛 부품 형식 표본은 figure-model 이 `ASSET_SCHEMA: asset: missing version` 으로 거절해
31
+ * 하나도 열리지 않았다(2026-10-13). 이제 저장소의 V3 자산(시험 고정물 · 레시피 기본 모델)을 그대로 읽는다.
32
+ */
33
+ const { currentSamples } = await import(pathToFileURL(REAL).href)
34
+ const { parseFigureAsset } = await import('@hatiolab/figure-model')
35
+ const samples = currentSamples()
36
+ const refused: string[] = []
37
+
38
+ for (const sample of samples) {
39
+ try {
40
+ parseFigureAsset(JSON.stringify(sample.source))
41
+ } catch (error) {
42
+ refused.push(`${sample.key}: ${(error as Error).message}`)
43
+ }
44
+ }
45
+
46
+ assert.ok(samples.length > 10, `표본이 ${samples.length} 개뿐이다`)
47
+ assert.equal(new Set(samples.map(s => s.key)).size, samples.length, '표본 키가 겹친다')
48
+ assert.deepEqual(refused, [])
49
+ })