@things-factory/figure-ui 10.1.158 → 10.1.160

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 (39) hide show
  1. package/client/graphql/index.ts +38 -0
  2. package/client/harness-theme/brand.css +21 -25
  3. package/client/index.ts +1 -0
  4. package/client/modeller/figure-clips.ts +17 -8
  5. package/client/modeller/figure-links.ts +21 -6
  6. package/client/modeller/figure-preview.ts +21 -7
  7. package/client/modeller/links.ts +13 -0
  8. package/client/setting-let/figure-namespace-setting-let.ts +84 -0
  9. package/dist-client/graphql/index.d.ts +13 -0
  10. package/dist-client/graphql/index.js +24 -0
  11. package/dist-client/graphql/index.js.map +1 -1
  12. package/dist-client/harness-theme/brand.css +21 -25
  13. package/dist-client/index.d.ts +1 -0
  14. package/dist-client/index.js +1 -0
  15. package/dist-client/index.js.map +1 -1
  16. package/dist-client/modeller/figure-clips.js +26 -18
  17. package/dist-client/modeller/figure-clips.js.map +1 -1
  18. package/dist-client/modeller/figure-links.js +21 -7
  19. package/dist-client/modeller/figure-links.js.map +1 -1
  20. package/dist-client/modeller/figure-preview.js +21 -7
  21. package/dist-client/modeller/figure-preview.js.map +1 -1
  22. package/dist-client/modeller/links.d.ts +13 -0
  23. package/dist-client/modeller/links.js +11 -0
  24. package/dist-client/modeller/links.js.map +1 -1
  25. package/dist-client/setting-let/figure-namespace-setting-let.d.ts +18 -0
  26. package/dist-client/setting-let/figure-namespace-setting-let.js +93 -0
  27. package/dist-client/setting-let/figure-namespace-setting-let.js.map +1 -0
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +5 -4
  30. package/test/brand-palette.test.ts +41 -6
  31. package/test/data-motion.test.ts +3 -0
  32. package/test/harness-serve.test.ts +49 -0
  33. package/test/links.test.ts +29 -0
  34. package/translations/en.json +8 -1
  35. package/translations/es.json +8 -1
  36. package/translations/ja.json +8 -1
  37. package/translations/ko.json +8 -1
  38. package/translations/ms.json +8 -1
  39. package/translations/zh.json +8 -1
@@ -211,6 +211,44 @@ export async function releaseFigure(id: string, comment?: string): Promise<Figur
211
211
  return unwrap<Figure>(response, 'releaseFigure')
212
212
  }
213
213
 
214
+ /** A domain's figure namespace as the setting screen shows it (ADR-0114). */
215
+ export interface FigureNamespaceState {
216
+ /** What new figures get; null when neither the setting nor the subdomain is a namespace. */
217
+ namespace: string | null
218
+ /** The setting as written; empty when the subdomain is used. */
219
+ written: string
220
+ subdomain: string
221
+ /** The domain's figures carry it: it cannot be set any more. */
222
+ fixed: boolean
223
+ }
224
+
225
+ const FIGURE_NAMESPACE_FIELDS = 'namespace written subdomain fixed'
226
+
227
+ export async function fetchFigureNamespace(): Promise<FigureNamespaceState> {
228
+ const response = await client.query({
229
+ query: gql`
230
+ query {
231
+ figureNamespace { ${FIGURE_NAMESPACE_FIELDS} }
232
+ }
233
+ `,
234
+ fetchPolicy: 'network-only'
235
+ })
236
+ return unwrap<FigureNamespaceState>(response, 'figureNamespace')
237
+ }
238
+
239
+ /** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */
240
+ export async function setFigureNamespace(namespace: string): Promise<FigureNamespaceState> {
241
+ const response = await client.mutate({
242
+ mutation: gql`
243
+ mutation ($namespace: String!) {
244
+ setFigureNamespace(namespace: $namespace) { ${FIGURE_NAMESPACE_FIELDS} }
245
+ }
246
+ `,
247
+ variables: { namespace }
248
+ })
249
+ return unwrap<FigureNamespaceState>(response, 'setFigureNamespace')
250
+ }
251
+
214
252
  /** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */
215
253
  export interface KeptDraft {
216
254
  id: string
@@ -1,13 +1,13 @@
1
1
  /*
2
- * figure 의 **브랜드 색 — 정본은 이 파일 하나다.**
2
+ * figure 하네스의 **브랜드 색 이름 — 값은 operato-suite 의 teal 틀이 갖는다.**
3
3
  *
4
4
  * ── 왜 이 파일이 생겼나 ────────────────────────────────────────────────────
5
5
  * figure-ui 에는 팔레트가 없었다. 호스트 앱이 주는 `--md-sys-color-*` 를 그대로 받아 썼고,
6
- * 하네스(`test/browser/modeller-page.mjs`)는 자기 `:root` 에 값을 직접 적고 있었다. 그래서
6
+ * 그때의 하네스 픽스처는 자기 `:root` 에 값을 직접 적고 있었다. 그래서
7
7
  * 하네스에서 본 색과 앱에서 볼 색이 **아무 오류 없이 달라질 수 있는** 상태였다.
8
8
  *
9
9
  * operato-plant 가 같은 문제를 겪고 같은 방식으로 풀었다(`operato-plant/assets/brand.css`).
10
- * 그 구조를 그대로 따른다 — 값은 여기 한 번만 적고, 앱 팔레트와 하네스가 이 파일을 읽는다.
10
+ * 그 구조를 따른다 — 하네스 팔레트는 이 파일의 이름만 읽고, 이 파일은 값을 suite 에서 들여온다.
11
11
  *
12
12
  * ── 왜 `themes/` 가 아닌가 ─────────────────────────────────────────────────
13
13
  * `@things-factory/builder` 의 `ThemeOverridePlugin` 이 모든 모듈의 `client/themes/*.css` 를
@@ -17,16 +17,22 @@
17
17
  * 모듈이 선언하지 않는다 — shell 이 열고 앱이 채운다(ADR-0060 결정 2).
18
18
  *
19
19
  * 이 폴더의 셋은 **하네스가 쓰는 것**이고, `themes` 밖에 있으므로 플러그인이 아예 보지 않는다.
20
- * 앱 화면의 색은 앱의 테마 파일이 정한다. 여기 값을 앱 색으로 알고 고치지 말 것 — 지금 이 셋은
21
- * navy 이고 앱은 teal 이다(수석 아키텍트가 사용자께 올려 둔 미결이다).
20
+ * 앱 화면의 색은 앱의 테마 파일이 정한다.
21
+ *
22
+ * ── 값은 여기 없다 ─────────────────────────────────────────────────────────
23
+ * teal 두 값은 `@things-factory/operato-suite` 의 `frames/teal.css` 가 `:root` 에 한 번 적는다
24
+ * (`--teal-on-light` · `--teal-on-dark`). operato-figure 와 operato-twin 의 앱 화면이 읽는 바로 그
25
+ * 값이다. 이 파일은 그것을 들여와 하네스의 이름으로 잇기만 한다 — 하네스는 시험 도구이고, 패키지
26
+ * 의존이 아니라 저장소 안의 경로로 읽는다(`/pkg/` 가 packages 폴더다). 설치본에서도 두 패키지는
27
+ * 형제 폴더라 같은 상대 경로가 맞는다.
22
28
  *
23
29
  * ── 브랜드 색만 여기 둔다 ──────────────────────────────────────────────────
24
30
  * 중성색은 여기 없다. 그것은 `client/harness-theme/light.css` 가 갖는다. 여기 있는 것은 **제품을
25
31
  * 가리키는 색**뿐이고, 그것만 절대 갈라지면 안 된다.
26
32
  *
27
33
  * ── 밝은 것과 어두운 것을 함께 든다 ────────────────────────────────────────
28
- * 어두운 바탕에서 `#00727e` 는 가라앉고(대비 3.21), 밝은 바탕에서 `#22c3cf` 는 글자에 못 쓴다
29
- * (대비 2.15). 그래서 같은 이름이 밝기마다 다른 값을 갖는다 — 이름이 뜻이고 값은 자리에 따라
34
+ * 어두운 바탕에서 밝은 면용 teal 은 가라앉고(대비 3.21), 밝은 바탕에서 어두운 면용 teal 은 글자에
35
+ * 못 쓴다(대비 2.15). 그래서 같은 이름이 밝기마다 다른 값을 갖는다 — 이름이 뜻이고 값은 자리에 따라
30
36
  * 다르다. plant · twin 과 같은 규약이다.
31
37
  *
32
38
  * ── 색은 하나뿐이다 ────────────────────────────────────────────────────────
@@ -38,31 +44,21 @@
38
44
  * 팔레트 파일에 있다.
39
45
  */
40
46
 
47
+ @import url('../../../operato-suite/client/frames/teal.css');
48
+
41
49
  :root {
42
50
  /*
43
- * figure — 딥 틸 186°.
44
- *
45
- * 흰 글자 대비 5.66 으로 작은 글자 기준(4.5)을 넘는다. [측정 2026-09-19]
46
- *
47
- * **이 값은 앱이 화면에 쓰는 것과 같다**(`operato-figure` 의 팔레트 · `assets/brand.css` 의
48
- * `--brand-figure`). 한동안 이 파일은 네이비 `#154473` 이었고 앱은 틸이었다 — 이 파일이 테마
49
- * 묶음에 실리지 않고 하네스만 읽었으므로 아무 오류 없이 두 얼굴이었다. 그것을 막으려고 생긴
50
- * 파일이 그것을 못 막고 있었다. 2026-09-19 사용자 결정으로 앱 쪽 값에 맞췄다.
51
+ * figure — 딥 틸 186°. 흰 글자 대비 5.66 으로 작은 글자 기준(4.5)을 넘는다.
51
52
  *
52
53
  * 채도를 낮게 잡은 것이 이 색의 요점이다. 이 도구의 화면에서 색이 말해야 하는 것은 **저작자가
53
54
  * 고른 부품 색**이다. 틀이 진한 색으로 소리를 내면 캔버스 안의 색을 판단할 수 없다. 그래서
54
55
  * 브랜드 색은 깊고 조용하게 두고, 고른 것의 밑줄 · 기본 단추 · 링크에만 좁게 쓴다.
55
56
  */
56
- --figure-brand: #00727e;
57
+ --figure-brand: var(--teal-on-light);
57
58
 
58
- /*
59
- * 어두운 면 위에서 쓰는 같은 색. 어두운 면(`#12161b`) 대비 8.45. [측정 2026-09-19]
60
- *
61
- * 3D 캔버스의 선택 테두리와 어두운 배경의 강조가 이 값이다. 앱 팔레트의 어두운 쪽 브랜드 색과
62
- * 같다.
63
- */
64
- --figure-brand-2: #22c3cf;
59
+ /* 어두운 면 위에서 쓰는 같은 색. 3D 캔버스의 선택 테두리와 어두운 배경의 강조가 이 값이다. */
60
+ --figure-brand-2: var(--teal-on-dark);
65
61
 
66
- /** 아주 옅게 깔 때 — 고른 줄의 바탕. 위 색을 흰 바탕에 8% 로 깐 값이다. */
67
- --figure-brand-wash: #ebf4f5;
62
+ /* 아주 옅게 깔 때 — 고른 줄의 바탕. 위 색을 흰 바탕에 8% 로 깐 값이다. */
63
+ --figure-brand-wash: color-mix(in srgb, var(--teal-on-light) 8%, white);
68
64
  }
package/client/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './types.js'
2
2
  export * from './graphql/index.js'
3
3
  export * from './figure-assistant.js'
4
+ export * from './setting-let/figure-namespace-setting-let.js'
@@ -10,6 +10,9 @@ import { clipGroupsOf, clipsOf, freeGroupIdOf, groupEdit, poseAt, trackEdit, und
10
10
  import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
11
11
  import { HelpToggles } from './help.js'
12
12
 
13
+ /** A value as a person reads it: at most three decimals, the float's last digits (14.399999999999999) left out. */
14
+ const shown = (value: number) => value.toLocaleString(undefined, { maximumFractionDigits: 3 })
15
+
13
16
  /**
14
17
  * 클립 — controls played by keys over time. A clip is one clock; each track plays one
15
18
  * control. Scrubbing a clip shows its pose at that moment (one batch of input changes, not saved as the default); a key
@@ -108,13 +111,16 @@ export class FigureClips extends localize(i18next)(LitElement) {
108
111
  const asset = this.asset
109
112
  if (!asset) return nothing
110
113
  const clips = clipsOf(asset)
114
+ /* Nothing to play yet: said inside the section it belongs to, with the section's own margins. */
115
+ const empty = !clips.length && !undrivenOf(asset).length && !asset.motionGroups?.length
111
116
  return html`
112
117
  <section>
113
118
  <h3>${i18next.t('figure.label.clips')}${this.help.button('figure.text.clips-explained')}</h3>
114
119
  ${this.help.hint('figure.text.clips-explained')}
115
120
  ${clips.map(clip => this.renderClip(clip))}
121
+ ${empty ? html`<p hint>${i18next.t('figure.text.no-controls')}</p>` : nothing}
116
122
  </section>
117
- ${this.renderAdd(asset, clips)} ${this.renderGroups(asset)}
123
+ ${empty ? nothing : this.renderAdd(asset, clips)} ${this.renderGroups(asset)}
118
124
  `
119
125
  }
120
126
 
@@ -166,7 +172,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
166
172
  <div keys>
167
173
  ${track.keys.map(
168
174
  (key, i) => html`<span key
169
- >${key.seconds}s → ${key.value}
175
+ >${key.seconds}s → ${shown(key.value)}
170
176
  <button
171
177
  ?disabled=${this.locked || !withoutKey(track.keys, i)}
172
178
  title=${i18next.t('figure.button.remove-key')}
@@ -183,7 +189,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
183
189
  ?disabled=${this.locked || current === undefined}
184
190
  @click=${() => this.edit(trackEdit(clip, track, { keys: withKey(track.keys, { seconds: Math.round(at * 1000) / 1000, value: current! }) }))}
185
191
  >
186
- <md-icon>add</md-icon>${i18next.t('figure.button.add-key-here', { seconds: at.toFixed(2), value: current ?? '' })}
192
+ <md-icon>add</md-icon>${i18next.t('figure.button.add-key-here', { seconds: at.toFixed(2), value: current === undefined ? '' : shown(current) })}
187
193
  </button>
188
194
  </div>`
189
195
  }
@@ -195,12 +201,13 @@ export class FigureClips extends localize(i18next)(LitElement) {
195
201
  const group = joining ? undefined : groups.find(g => g.id === a.group)
196
202
  /* The controls this clip may take: not played by it yet, and by no clip outside its group. */
197
203
  const free = undrivenOf(asset, joining?.name ?? '', group?.id)
198
- if (!free.length && !clips.length && !groups.length) return html`<p hint>${i18next.t('figure.text.no-controls')}</p>`
199
204
  const stateId = free.includes(a.state) ? a.state : free[0]
200
205
  const name = joining ? joining.name : a.name.trim()
201
206
  const input = asset.document.model.inputs.find(i => i.id === stateId)
202
207
  const duration = joining ? joining.duration : a.duration
203
- const ready = !!name && duration > 0 && !!input
208
+ /* A new clip takes a name no clip has: the same name would rewrite that clip's track, not add one. */
209
+ const taken = !joining && clips.some(c => c.name === name)
210
+ const ready = !!name && !taken && duration > 0 && !!input
204
211
  return html`<section><h3>${i18next.t('figure.label.add-track')}</h3>
205
212
  <div form>
206
213
  <span>${i18next.t('figure.label.controls')}</span>
@@ -229,7 +236,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
229
236
  <button
230
237
  act
231
238
  ?disabled=${this.locked || !ready}
232
- @click=${() =>
239
+ @click=${() => {
233
240
  this.edit({
234
241
  kind: 'set-driver',
235
242
  state: stateId,
@@ -240,7 +247,10 @@ export class FigureClips extends localize(i18next)(LitElement) {
240
247
  { seconds: 0, value: input!.min },
241
248
  { seconds: duration, value: input!.max }
242
249
  ]
243
- })}
250
+ })
251
+ /* The name was this clip's; the next new clip is named again. */
252
+ this.adding = { ...a, name: '', state: '' }
253
+ }}
244
254
  >
245
255
  <md-icon>add</md-icon>${i18next.t('figure.button.add-track')}
246
256
  </button></section>`
@@ -287,7 +297,6 @@ export class FigureClips extends localize(i18next)(LitElement) {
287
297
  return html`<section clip>
288
298
  <div track-head>
289
299
  <strong>${group.label}</strong>
290
- <span></span>
291
300
  <button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-motion-group')} @click=${() => this.edit({ kind: 'remove-motion-group', id: group.id })}>
292
301
  <md-icon>close</md-icon>
293
302
  </button>
@@ -28,6 +28,7 @@ import {
28
28
  visibilityControlsOf,
29
29
  placeKeptOf,
30
30
  fasteningOf,
31
+ fastenGapOf,
31
32
  hasInstanceSize,
32
33
  holdsThings,
33
34
  leadersFor,
@@ -73,6 +74,7 @@ const freshFasten = () => ({
73
74
  targetFace: 'top' as FigureFace,
74
75
  facing: 'meet' as 'meet' | 'flush',
75
76
  gap: 0,
77
+ gapKept: undefined as unknown,
76
78
  twist: 0,
77
79
  align: {} as Partial<Record<Axis, Alignment>>,
78
80
  alignMm: {} as Partial<Record<Axis, number>>,
@@ -482,11 +484,14 @@ export class FigureLinks extends localize(i18next)(LitElement) {
482
484
  private heldWords(asset: FigureAsset) {
483
485
  const joint = (asset.joints ?? []).find(j => j.body1.part === this.partId)
484
486
  const face = joint ? i18next.t(endPlaceKeyOf(joint.body1)) : ''
485
- const gap = typeof joint?.origin?.gap === 'number' ? joint.origin.gap : 0
487
+ const gap = joint?.origin?.gap ?? 0
488
+ const place = i18next.t(`figure.label.resize-place-${asset.placement ?? 'floor'}`)
486
489
  const where = !joint
487
490
  ? i18next.t('figure.text.held-by-rule')
488
491
  : 'plane' in joint.body0
489
- ? i18next.t('figure.text.held-on-plane', { face, place: i18next.t(`figure.label.resize-place-${asset.placement ?? 'floor'}`), gap: String(gap) })
492
+ ? typeof gap === 'number'
493
+ ? i18next.t('figure.text.held-on-plane', { face, place, gap: String(gap) })
494
+ : i18next.t('figure.text.held-on-plane-worked-out', { face, place })
490
495
  : i18next.t('figure.text.held-on-part', { face, other: joint.body0.part, otherFace: i18next.t(endPlaceKeyOf(joint.body0)) })
491
496
  return where
492
497
  }
@@ -573,7 +578,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
573
578
  face: myFace,
574
579
  to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
575
580
  ...(f.toPlane ? {} : { facing: f.facing }),
576
- ...(f.gap ? { gap: f.gap } : {}),
581
+ ...fastenGapOf(f),
577
582
  twist: f.twist,
578
583
  ...(align ? { align } : {}),
579
584
  replace: !!held
@@ -586,7 +591,11 @@ export class FigureLinks extends localize(i18next)(LitElement) {
586
591
  for the default.
587
592
  */
588
593
  const detail = [
589
- ...(f.gap ? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })] : []),
594
+ ...(f.gapKept !== undefined
595
+ ? [i18next.t('figure.text.fasten-summary-gap-worked-out')]
596
+ : f.gap
597
+ ? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })]
598
+ : []),
590
599
  ...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
591
600
  ]
592
601
  /* Across the face, a row only where the drawing cannot set it: no drawing of the face, or a share or a field to follow. */
@@ -672,7 +681,13 @@ export class FigureLinks extends localize(i18next)(LitElement) {
672
681
  ${alignRows.length ? this.renderAlignment(alignRows, settle) : nothing}
673
682
  <div form>
674
683
  <span>${i18next.t('figure.label.gap')}</span>
675
- <input type="number" step="any" .value=${String(f.gap)} @change=${(e: Event) => ((this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0 }), settle())} />
684
+ <input
685
+ type="number"
686
+ step="any"
687
+ .value=${f.gapKept !== undefined ? '' : String(f.gap)}
688
+ placeholder=${f.gapKept !== undefined ? i18next.t('figure.label.follows-expression') : ''}
689
+ @change=${(e: Event) => ((this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined }), settle())}
690
+ />
676
691
  <span>${i18next.t('figure.label.twist')}</span>
677
692
  <input type="number" step="any" .value=${String(f.twist)} @change=${(e: Event) => ((this.fasten = { ...f, twist: Number((e.target as HTMLInputElement).value) || 0 }), settle())} />
678
693
  </div>
@@ -696,7 +711,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
696
711
  const f = this.fasten
697
712
  if ('targetFace' in c) this.aimAt(asset, { ...f, targetFace: c.targetFace as FigureFace })
698
713
  else if ('mode' in c) this.fasten = { ...f, face: c.mode === 'centre' ? 'centre' : f.face === 'centre' ? 'bottom' : f.face, facing: c.mode === 'flush' ? 'flush' : 'meet' }
699
- else if ('gap' in c) this.fasten = { ...f, gap: c.gap }
714
+ else if ('gap' in c) this.fasten = { ...f, gap: c.gap, gapKept: undefined }
700
715
  else
701
716
  this.fasten = {
702
717
  ...f,
@@ -113,6 +113,16 @@ export class FigurePreview extends localize(i18next)(LitElement) {
113
113
  align-items: center;
114
114
  gap: 8px;
115
115
  }
116
+ /* A motion group's clip: as wide as the dial, slider and value together, so its name reads. */
117
+ div[clips] select {
118
+ grid-column: 2 / -1;
119
+ min-width: 0;
120
+ font: inherit;
121
+ color: inherit;
122
+ background-color: rgba(255, 255, 255, 0.14);
123
+ border: 1px solid rgba(255, 255, 255, 0.22);
124
+ border-radius: 5px;
125
+ }
116
126
  div[clips] svg[dial] {
117
127
  width: 20px;
118
128
  height: 20px;
@@ -296,13 +306,17 @@ export class FigurePreview extends localize(i18next)(LitElement) {
296
306
  ${(asset.motionGroups ?? []).map(group => {
297
307
  const playing = this.moving[group.id] ?? { clip: group.defaultClip, speed: 0 }
298
308
  return html`<label>
299
- <span>${group.label} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
300
- <select aria-label=${group.label} @change=${(e: Event) => this.move(group.id, { ...playing, clip: (e.target as HTMLSelectElement).value })}>
301
- ${group.clips.map(c => html`<option value=${c} ?selected=${c === playing.clip}>${c}</option>`)}
302
- </select>
303
- <input type="range" min="0" max="3" step="0.05" .value=${String(playing.speed)} @input=${(e: Event) => this.move(group.id, { ...playing, speed: Number((e.target as HTMLInputElement).value) })} />
304
- <output>${playing.speed.toFixed(2)}</output>
305
- </label>`
309
+ <span>${group.label} <span drive>${i18next.t('figure.label.clip')}</span></span>
310
+ <select aria-label=${group.label} @change=${(e: Event) => this.move(group.id, { ...playing, clip: (e.target as HTMLSelectElement).value })}>
311
+ ${group.clips.map(c => html`<option value=${c} ?selected=${c === playing.clip}>${c}</option>`)}
312
+ </select>
313
+ </label>
314
+ <label>
315
+ <span>${group.label} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
316
+ <span></span>
317
+ <input type="range" min="0" max="3" step="0.05" .value=${String(playing.speed)} @input=${(e: Event) => this.move(group.id, { ...playing, speed: Number((e.target as HTMLInputElement).value) })} />
318
+ <output>${playing.speed.toFixed(2)}</output>
319
+ </label>`
306
320
  })}
307
321
  </div>`
308
322
  }
@@ -387,6 +387,8 @@ export function fasteningOf(
387
387
  targetFace: string
388
388
  facing: 'meet' | 'flush'
389
389
  gap: number
390
+ /** The record's gap when it is worked out (a length that follows something): kept as it is until a number is typed. */
391
+ gapKept?: unknown
390
392
  twist: number
391
393
  align: Partial<Record<Axis, Alignment>>
392
394
  alignMm: Partial<Record<Axis, number>>
@@ -424,12 +426,23 @@ export function fasteningOf(
424
426
  targetFace,
425
427
  facing: record.origin?.facing ?? 'meet',
426
428
  gap: typeof gap === 'number' ? gap : 0,
429
+ ...(gap !== undefined && typeof gap !== 'number' ? { gapKept: gap } : {}),
427
430
  twist,
428
431
  ...kept,
429
432
  ...(atOrigin ? { atOrigin: true as const } : {})
430
433
  }
431
434
  }
432
435
 
436
+ /**
437
+ * The `gap` an attach command writes from the form: a worked-out gap as it was read, else the number, else none. Read as
438
+ * a number, a gap that follows the legs or the instance height became 0, and opening the form on the part dropped it
439
+ * to the plane (2026-10-13, the robot dog and the forklift).
440
+ */
441
+ export function fastenGapOf(form: { gap: number; gapKept?: unknown }): { gap?: unknown } {
442
+ if (form.gapKept !== undefined) return { gap: form.gapKept }
443
+ return form.gap ? { gap: form.gap } : {}
444
+ }
445
+
433
446
  /**
434
447
  * The alignment that keeps the part where it stands when it is fastened: on each axis across the face, its distance
435
448
  * from the target's centre (the plane's is the origin). Fastening used to centre it -- a door set 100 mm in front of
@@ -0,0 +1,84 @@
1
+ import '@material/web/textfield/filled-text-field.js'
2
+ import '@things-factory/setting-base'
3
+
4
+ import { css, html, LitElement } from 'lit'
5
+ import { customElement, state } from 'lit/decorators.js'
6
+
7
+ import { i18next, localize } from '@operato/i18n'
8
+
9
+ import { fetchFigureNamespace, setFigureNamespace, type FigureNamespaceState } from '../graphql/index.js'
10
+
11
+ /**
12
+ * The domain's figure namespace (ADR-0114): the `{namespace}` of every full name `{namespace}/{type}` a board stores.
13
+ * Empty takes the subdomain. It is set once: when the domain's figures carry it, it is shown and no longer edited --
14
+ * the server refuses a change either way, and says why.
15
+ */
16
+ @customElement('figure-namespace-setting-let')
17
+ export class FigureNamespaceSettingLet extends localize(i18next)(LitElement) {
18
+ static styles = [
19
+ css`
20
+ div[slot='content'] {
21
+ display: grid;
22
+ gap: var(--spacing-small, 4px);
23
+ }
24
+ md-filled-text-field {
25
+ width: 100%;
26
+ }
27
+ strong {
28
+ font: var(--label-font);
29
+ color: var(--md-sys-color-on-surface);
30
+ }
31
+ p {
32
+ margin: 0;
33
+ font-size: 0.8rem;
34
+ color: var(--md-sys-color-on-surface-variant);
35
+ text-transform: none;
36
+ }
37
+ `
38
+ ]
39
+
40
+ @state() private now?: FigureNamespaceState
41
+ @state() private refused = ''
42
+
43
+ render() {
44
+ const now = this.now
45
+ return html`
46
+ <setting-let>
47
+ <span slot="title">${i18next.t('figure.title.namespace-setting')}</span>
48
+ <div slot="content">
49
+ ${!now
50
+ ? html``
51
+ : now.fixed
52
+ ? html`<strong>${now.namespace}</strong>
53
+ <p>${i18next.t('figure.text.namespace-fixed')}</p>`
54
+ : html`<md-filled-text-field
55
+ .label=${i18next.t('figure.label.namespace')}
56
+ .value=${now.written}
57
+ placeholder=${now.subdomain}
58
+ ?error=${!!this.refused || !now.namespace}
59
+ error-text=${this.refused || i18next.t('figure.text.namespace-subdomain-unusable', { subdomain: now.subdomain })}
60
+ supporting-text=${i18next.t('figure.text.namespace-empty-takes-subdomain', { subdomain: now.subdomain })}
61
+ @change=${(e: Event) => this.save((e.target as HTMLInputElement).value)}
62
+ ></md-filled-text-field>`}
63
+ </div>
64
+ </setting-let>
65
+ `
66
+ }
67
+
68
+ async firstUpdated() {
69
+ try {
70
+ this.now = await fetchFigureNamespace()
71
+ } catch (e) {
72
+ this.refused = (e as Error).message
73
+ }
74
+ }
75
+
76
+ private async save(value: string) {
77
+ try {
78
+ this.now = await setFigureNamespace(value)
79
+ this.refused = ''
80
+ } catch (e) {
81
+ this.refused = (e as Error).message
82
+ }
83
+ }
84
+ }
@@ -39,6 +39,19 @@ export declare function deleteFigure(id: string): Promise<boolean>;
39
39
  * 발행을 푼다(초안으로 돌아온다).
40
40
  */
41
41
  export declare function releaseFigure(id: string, comment?: string): Promise<Figure>;
42
+ /** A domain's figure namespace as the setting screen shows it (ADR-0114). */
43
+ export interface FigureNamespaceState {
44
+ /** What new figures get; null when neither the setting nor the subdomain is a namespace. */
45
+ namespace: string | null;
46
+ /** The setting as written; empty when the subdomain is used. */
47
+ written: string;
48
+ subdomain: string;
49
+ /** The domain's figures carry it: it cannot be set any more. */
50
+ fixed: boolean;
51
+ }
52
+ export declare function fetchFigureNamespace(): Promise<FigureNamespaceState>;
53
+ /** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */
54
+ export declare function setFigureNamespace(namespace: string): Promise<FigureNamespaceState>;
42
55
  /** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */
43
56
  export interface KeptDraft {
44
57
  id: string;
@@ -170,6 +170,30 @@ export async function releaseFigure(id, comment) {
170
170
  });
171
171
  return unwrap(response, 'releaseFigure');
172
172
  }
173
+ const FIGURE_NAMESPACE_FIELDS = 'namespace written subdomain fixed';
174
+ export async function fetchFigureNamespace() {
175
+ const response = await client.query({
176
+ query: gql `
177
+ query {
178
+ figureNamespace { ${FIGURE_NAMESPACE_FIELDS} }
179
+ }
180
+ `,
181
+ fetchPolicy: 'network-only'
182
+ });
183
+ return unwrap(response, 'figureNamespace');
184
+ }
185
+ /** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */
186
+ export async function setFigureNamespace(namespace) {
187
+ const response = await client.mutate({
188
+ mutation: gql `
189
+ mutation ($namespace: String!) {
190
+ setFigureNamespace(namespace: $namespace) { ${FIGURE_NAMESPACE_FIELDS} }
191
+ }
192
+ `,
193
+ variables: { namespace }
194
+ });
195
+ return unwrap(response, 'setFigureNamespace');
196
+ }
173
197
  const KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`;
174
198
  /**
175
199
  * 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport { reached, refusedIn } from './reached.js'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n namespace\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */\nexport interface KeptDraft {\n id: string\n kind: 'revert' | 'undo'\n state?: string\n version?: number\n revertedToVersion?: number\n restoredAt?: string\n createdAt?: string\n creator?: { name?: string }\n}\n\nconst KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`\n\n/**\n * 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가\n * 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면\n * (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.\n */\nexport async function revertFigureVersion(\n id: string,\n version: number,\n requestId: string,\n expectedSource: string\n): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!, $requestId: String!, $expectedSource: String!) {\n revertFigureVersion(id: $id, version: $version, requestId: $requestId, expectedSource: $expectedSource) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { id, version, requestId, expectedSource }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'revertFigureVersion')\n}\n\n/** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */\nexport async function undoFigureRevert(keptId: string, requestId: string): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation UndoFigureRevert($keptId: String!, $requestId: String!) {\n undoFigureRevert(keptId: $keptId, requestId: $requestId) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { keptId, requestId }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'undoFigureRevert')\n}\n\n/** A figure an AI proposal was saved into (ADR-0097 §5.6 item 5). */\nexport interface ProposalOutcome {\n figureId: string\n type?: string\n name?: string\n recordedAt?: string\n}\n\n/**\n * 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 도형들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,\n * 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.\n */\nexport async function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]> {\n const response = await client.query({\n query: gql`\n query FigureProposalOutcome($proposalKey: String!) {\n figureProposalOutcome(proposalKey: $proposalKey) { figureId type name recordedAt }\n }\n `,\n variables: { proposalKey },\n fetchPolicy: 'network-only'\n })\n return unwrap<ProposalOutcome[]>(response, 'figureProposalOutcome') ?? []\n}\n\n/** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */\nexport async function fetchReplacedDrafts(id: string): Promise<KeptDraft[]> {\n const response = await client.query({\n query: gql`\n query FigureReplacedDrafts($id: String!) {\n figureReplacedDrafts(id: $id) { ${KEPT_FIELDS} }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n return unwrap<KeptDraft[]>(response, 'figureReplacedDrafts')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/**\n * 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.\n *\n * 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는\n * 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —\n * 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행판을 읽는다.\n */\nexport async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigureDraft($source: String!, $id: String) {\n inspectFigureDraft(source: $source, id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n return (await fetchReleasedFigure(id))?.source\n}\n\n/** Read-only catalogue preview: source and version come from the same released snapshot. */\nexport async function fetchReleasedFigure(id: string): Promise<{ version: number; source: string } | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ version: number; source: string } | undefined>(response, 'figureReleased') ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n\n/** A recipe template as the server gives it (ADR-0095, amended): words in the language asked, the recipe as JSON. */\nexport interface RecipeTemplateRow {\n name: string\n category: string\n tags: string[]\n target: 'part' | 'figure'\n title: string\n description: string\n source: string\n hasThumbnail: boolean\n}\n\nexport async function fetchRecipeTemplates(lang: string): Promise<RecipeTemplateRow[]> {\n const response = await client.query({\n query: gql`\n query ($lang: String) {\n figureRecipeTemplates(lang: $lang) {\n name\n category\n tags\n target\n title\n description\n source\n hasThumbnail\n }\n }\n `,\n variables: { lang }\n })\n return unwrap<RecipeTemplateRow[] | null>(response, 'figureRecipeTemplates') ?? []\n}\n\n/** One template's guide in the language asked, or '' while none is written. */\nexport async function fetchRecipeGuide(name: string, lang: string): Promise<string> {\n const response = await client.query({\n query: gql`\n query ($name: String!, $lang: String) {\n figureRecipeTemplate(name: $name, lang: $lang) {\n guide\n }\n }\n `,\n variables: { name, lang }\n })\n return unwrap<{ guide?: string } | null>(response, 'figureRecipeTemplate')?.guide ?? ''\n}\n\n/**\n * A GLB for a mesh part (ADR-0129): the server reads it, keeps it by its hash and answers what it holds. A file a board\n * cannot draw is refused with the reason in the screen's words.\n */\nexport async function uploadFigureMesh(file: File): Promise<import('../modeller/mesh-import.js').MeshUpload> {\n const response = await client.mutate({\n mutation: gql`\n mutation UploadFigureMesh($file: Upload!) {\n uploadFigureMesh(file: $file) {\n hash\n summary\n }\n }\n `,\n variables: { file },\n context: { hasUpload: true }\n })\n return unwrap(response, 'uploadFigureMesh')\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport { reached, refusedIn } from './reached.js'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n namespace\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** A domain's figure namespace as the setting screen shows it (ADR-0114). */\nexport interface FigureNamespaceState {\n /** What new figures get; null when neither the setting nor the subdomain is a namespace. */\n namespace: string | null\n /** The setting as written; empty when the subdomain is used. */\n written: string\n subdomain: string\n /** The domain's figures carry it: it cannot be set any more. */\n fixed: boolean\n}\n\nconst FIGURE_NAMESPACE_FIELDS = 'namespace written subdomain fixed'\n\nexport async function fetchFigureNamespace(): Promise<FigureNamespaceState> {\n const response = await client.query({\n query: gql`\n query {\n figureNamespace { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<FigureNamespaceState>(response, 'figureNamespace')\n}\n\n/** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */\nexport async function setFigureNamespace(namespace: string): Promise<FigureNamespaceState> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($namespace: String!) {\n setFigureNamespace(namespace: $namespace) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { namespace }\n })\n return unwrap<FigureNamespaceState>(response, 'setFigureNamespace')\n}\n\n/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */\nexport interface KeptDraft {\n id: string\n kind: 'revert' | 'undo'\n state?: string\n version?: number\n revertedToVersion?: number\n restoredAt?: string\n createdAt?: string\n creator?: { name?: string }\n}\n\nconst KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`\n\n/**\n * 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가\n * 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면\n * (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.\n */\nexport async function revertFigureVersion(\n id: string,\n version: number,\n requestId: string,\n expectedSource: string\n): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!, $requestId: String!, $expectedSource: String!) {\n revertFigureVersion(id: $id, version: $version, requestId: $requestId, expectedSource: $expectedSource) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { id, version, requestId, expectedSource }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'revertFigureVersion')\n}\n\n/** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */\nexport async function undoFigureRevert(keptId: string, requestId: string): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation UndoFigureRevert($keptId: String!, $requestId: String!) {\n undoFigureRevert(keptId: $keptId, requestId: $requestId) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { keptId, requestId }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'undoFigureRevert')\n}\n\n/** A figure an AI proposal was saved into (ADR-0097 §5.6 item 5). */\nexport interface ProposalOutcome {\n figureId: string\n type?: string\n name?: string\n recordedAt?: string\n}\n\n/**\n * 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 도형들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,\n * 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.\n */\nexport async function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]> {\n const response = await client.query({\n query: gql`\n query FigureProposalOutcome($proposalKey: String!) {\n figureProposalOutcome(proposalKey: $proposalKey) { figureId type name recordedAt }\n }\n `,\n variables: { proposalKey },\n fetchPolicy: 'network-only'\n })\n return unwrap<ProposalOutcome[]>(response, 'figureProposalOutcome') ?? []\n}\n\n/** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */\nexport async function fetchReplacedDrafts(id: string): Promise<KeptDraft[]> {\n const response = await client.query({\n query: gql`\n query FigureReplacedDrafts($id: String!) {\n figureReplacedDrafts(id: $id) { ${KEPT_FIELDS} }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n return unwrap<KeptDraft[]>(response, 'figureReplacedDrafts')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/**\n * 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.\n *\n * 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는\n * 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —\n * 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행판을 읽는다.\n */\nexport async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigureDraft($source: String!, $id: String) {\n inspectFigureDraft(source: $source, id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n return (await fetchReleasedFigure(id))?.source\n}\n\n/** Read-only catalogue preview: source and version come from the same released snapshot. */\nexport async function fetchReleasedFigure(id: string): Promise<{ version: number; source: string } | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ version: number; source: string } | undefined>(response, 'figureReleased') ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n\n/** A recipe template as the server gives it (ADR-0095, amended): words in the language asked, the recipe as JSON. */\nexport interface RecipeTemplateRow {\n name: string\n category: string\n tags: string[]\n target: 'part' | 'figure'\n title: string\n description: string\n source: string\n hasThumbnail: boolean\n}\n\nexport async function fetchRecipeTemplates(lang: string): Promise<RecipeTemplateRow[]> {\n const response = await client.query({\n query: gql`\n query ($lang: String) {\n figureRecipeTemplates(lang: $lang) {\n name\n category\n tags\n target\n title\n description\n source\n hasThumbnail\n }\n }\n `,\n variables: { lang }\n })\n return unwrap<RecipeTemplateRow[] | null>(response, 'figureRecipeTemplates') ?? []\n}\n\n/** One template's guide in the language asked, or '' while none is written. */\nexport async function fetchRecipeGuide(name: string, lang: string): Promise<string> {\n const response = await client.query({\n query: gql`\n query ($name: String!, $lang: String) {\n figureRecipeTemplate(name: $name, lang: $lang) {\n guide\n }\n }\n `,\n variables: { name, lang }\n })\n return unwrap<{ guide?: string } | null>(response, 'figureRecipeTemplate')?.guide ?? ''\n}\n\n/**\n * A GLB for a mesh part (ADR-0129): the server reads it, keeps it by its hash and answers what it holds. A file a board\n * cannot draw is refused with the reason in the screen's words.\n */\nexport async function uploadFigureMesh(file: File): Promise<import('../modeller/mesh-import.js').MeshUpload> {\n const response = await client.mutate({\n mutation: gql`\n mutation UploadFigureMesh($file: Upload!) {\n uploadFigureMesh(file: $file) {\n hash\n summary\n }\n }\n `,\n variables: { file },\n context: { hasUpload: true }\n })\n return unwrap(response, 'uploadFigureMesh')\n}\n"]}