@things-factory/figure-ui 10.1.69 → 10.1.71

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 (58) hide show
  1. package/client/graphql/index.ts +38 -6
  2. package/client/modeller/figure-v3-parts.ts +16 -136
  3. package/client/modeller/figure-v3-recipe-catalog.ts +348 -0
  4. package/client/modeller/v3-editor.ts +25 -5
  5. package/client/modeller/v3-fields.ts +21 -16
  6. package/client/modeller/v3-gizmo.ts +1 -1
  7. package/client/modeller/v3-links.ts +7 -66
  8. package/client/modeller/v3-palette.ts +3 -0
  9. package/client/modeller/v3-recipe-source.ts +15 -0
  10. package/client/modeller/v3-recipes.ts +53 -12
  11. package/client/pages/figure-v3-modeller-page.ts +16 -13
  12. package/dist-client/graphql/index.d.ts +12 -3
  13. package/dist-client/graphql/index.js +25 -6
  14. package/dist-client/graphql/index.js.map +1 -1
  15. package/dist-client/modeller/figure-v3-parts.d.ts +5 -12
  16. package/dist-client/modeller/figure-v3-parts.js +17 -147
  17. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  18. package/dist-client/modeller/figure-v3-recipe-catalog.d.ts +36 -0
  19. package/dist-client/modeller/figure-v3-recipe-catalog.js +382 -0
  20. package/dist-client/modeller/figure-v3-recipe-catalog.js.map +1 -0
  21. package/dist-client/modeller/v3-editor.d.ts +8 -3
  22. package/dist-client/modeller/v3-editor.js +35 -6
  23. package/dist-client/modeller/v3-editor.js.map +1 -1
  24. package/dist-client/modeller/v3-fields.d.ts +2 -2
  25. package/dist-client/modeller/v3-fields.js +20 -18
  26. package/dist-client/modeller/v3-fields.js.map +1 -1
  27. package/dist-client/modeller/v3-gizmo.js +1 -1
  28. package/dist-client/modeller/v3-gizmo.js.map +1 -1
  29. package/dist-client/modeller/v3-links.d.ts +8 -25
  30. package/dist-client/modeller/v3-links.js +4 -66
  31. package/dist-client/modeller/v3-links.js.map +1 -1
  32. package/dist-client/modeller/v3-palette.js +3 -0
  33. package/dist-client/modeller/v3-palette.js.map +1 -1
  34. package/dist-client/modeller/v3-recipe-source.d.ts +8 -0
  35. package/dist-client/modeller/v3-recipe-source.js +14 -0
  36. package/dist-client/modeller/v3-recipe-source.js.map +1 -0
  37. package/dist-client/modeller/v3-recipes.d.ts +25 -4
  38. package/dist-client/modeller/v3-recipes.js +43 -11
  39. package/dist-client/modeller/v3-recipes.js.map +1 -1
  40. package/dist-client/pages/figure-v3-modeller-page.d.ts +5 -3
  41. package/dist-client/pages/figure-v3-modeller-page.js +16 -13
  42. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  43. package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
  44. package/dist-client/tsconfig.tsbuildinfo +1 -1
  45. package/package.json +5 -4
  46. package/scripts/template-thumbnails.mjs +53 -0
  47. package/test/browser/build.mjs +3 -3
  48. package/test/browser/modeller-page.js +25 -3
  49. package/test/browser/modeller.check.mjs +57 -47
  50. package/test/browser/thumbnail-page.js +14 -0
  51. package/test/v3-fields.test.ts +5 -1
  52. package/test/v3-links.test.ts +12 -0
  53. package/test/v3-recipes.test.ts +44 -11
  54. package/translations/en.json +38 -1
  55. package/translations/ja.json +38 -1
  56. package/translations/ko.json +38 -1
  57. package/translations/ms.json +38 -1
  58. package/translations/zh.json +38 -1
@@ -42,22 +42,25 @@ export function groupFields(fields: readonly FieldLike[], part?: string): { vect
42
42
  return { vectors, singles }
43
43
  }
44
44
 
45
+ /**
46
+ * The part a copy copies: the member at the end of its path. A row repeats its part and what is fastened to it
47
+ * (ADR-0093, 2026-09-26), so the copies of one row are of several parts -- a roller's and the mark on it.
48
+ */
49
+ const memberOf = (path: unknown[]): string => {
50
+ const last = [...path].reverse().find((step): step is string => typeof step === 'string')!
51
+ return last.replace(/\.(placed|value)$/, '').replace(/\.repeat$/, '')
52
+ }
53
+
45
54
  /**
46
55
  * The name a person reads for a body. A repeated body's id is its path through the repeats (`["roller.repeat",0,
47
- * "roller.value"]`), which the list showed as it was; it reads as the repeat's name and the member's place counted
48
- * from 1 -- `roller 1`, and `rack 2·3` inside two repeats. Any other id is its own name.
56
+ * "roller.placed"]`), which the list showed as it was; it reads as the part it copies and its place counted from 1 --
57
+ * `roller 1`, and `mark 1` for the mark the first roller carries. Any other id is its own name.
49
58
  */
50
59
  export function partLabelOf(id: string): string {
51
- if (!id.startsWith('[')) return id
52
- let path: unknown
53
- try {
54
- path = JSON.parse(id)
55
- } catch {
56
- return id
57
- }
58
- if (!Array.isArray(path) || typeof path[0] !== 'string') return id
60
+ const path = pathOf(id)
61
+ if (!path) return id
59
62
  const places = path.filter((step): step is number => typeof step === 'number').map(n => n + 1)
60
- return places.length ? `${path[0].replace(/\.repeat$/, '')} ${places.join('·')}` : id
63
+ return places.length ? `${memberOf(path)} ${places.join('·')}` : id
61
64
  }
62
65
 
63
66
  /**
@@ -66,14 +69,16 @@ export function partLabelOf(id: string): string {
66
69
  */
67
70
  export function repeatOf(id: string, geometry: readonly unknown[] | undefined): { name: string; count: number; index: number } | null {
68
71
  const path = pathOf(id)
72
+ const paths = (geometry ?? []).map(g => pathOf((g as { id?: string }).id ?? '')).filter((p): p is unknown[] => !!p)
69
73
  if (!path) {
70
- /* The repeat itself, by its name: how many copies it makes now. */
71
- const copies = (geometry ?? []).filter(g => pathOf((g as { id?: string }).id ?? '')?.[0] === `${id}.repeat`).length
74
+ /* The part itself, by its name: how many copies of it the row makes now. */
75
+ const copies = paths.filter(p => memberOf(p) === id).length
72
76
  return copies ? { name: id, count: copies, index: 0 } : null
73
77
  }
74
- const count = (geometry ?? []).filter(g => pathOf((g as { id?: string }).id ?? '')?.[0] === path[0]).length
78
+ const name = memberOf(path)
79
+ const count = paths.filter(p => p[0] === path[0] && memberOf(p) === name).length
75
80
  const index = path.find((step): step is number => typeof step === 'number')
76
- return { name: String(path[0]).replace(/\.repeat$/, ''), count, index: (index ?? 0) + 1 }
81
+ return { name, count, index: (index ?? 0) + 1 }
77
82
  }
78
83
 
79
84
  function pathOf(id: string): unknown[] | null {
@@ -93,6 +98,6 @@ function pathOf(id: string): unknown[] | null {
93
98
  */
94
99
  export function ownerOf(id: string): string {
95
100
  const path = pathOf(id)
96
- return path ? String(path[0]).replace(/\.repeat$/, '') : id
101
+ return path ? memberOf(path) : id
97
102
  }
98
103
 
@@ -30,7 +30,7 @@ export interface GizmoReach {
30
30
  }
31
31
 
32
32
  /** Shapes whose first three arguments are the extent along X, Y and Z: a door frame's too (its width, height, depth). */
33
- const BOXES = new Set(['box-shape@1', 'rounded-box@1', 'portal-profile@2'])
33
+ const BOXES = new Set(['box-shape@1', 'rounded-box@1', 'portal-profile@2', 'hollow-box@2'])
34
34
 
35
35
  interface Node {
36
36
  id: string
@@ -1,5 +1,5 @@
1
- import { V3_FACES, v3ShapeFaces, v3PartsExtentOf, compileV3Asset, compileV3Graph, proposeV3Proportion, defaultInstanceSizeRange, foldInstanceSizeRange, v3AttachmentsOf, v3BindingsOf, v3InstanceSizeLimitsOf } from '@hatiolab/figure-model'
2
- import type { V3SizeRange } from '@hatiolab/figure-model'
1
+ import { V3_FACES, v3ShapeFaces, v3PartsExtentOf, compileV3Asset, compileV3Graph, v3FollowOf, proposeV3Proportion, defaultInstanceSizeRange, foldInstanceSizeRange, v3AttachmentsOf, v3BindingsOf, v3InstanceSizeLimitsOf } from '@hatiolab/figure-model'
2
+ import type { V3Follow, V3SizeRange } from '@hatiolab/figure-model'
3
3
  import type { V3Asset, V3BindingPlace, V3Face, V3Joint } from '@hatiolab/figure-model'
4
4
 
5
5
  /*
@@ -18,76 +18,17 @@ interface Node {
18
18
  const modelOf = (asset: V3Asset) =>
19
19
  asset.document.model as unknown as { nodes: Node[]; inputs: Array<{ id: string; role?: string; unit?: string; min?: number; max?: number }>; constants: Array<{ id: string; value: number }> }
20
20
 
21
- /** What one dimension of a part is worked out from. */
22
- export type Follow =
23
- | { of: 'own' }
24
- /** `times` 1 is the instance size itself; any other is a share of it. */
25
- | { of: 'instance'; axis: Axis; times: number; plus?: number }
26
- | { of: 'part'; part: string; axis: Axis; times?: number; plus?: number }
27
- | { of: 'input'; input: string }
28
- /** Worked out some other way (an expression) — shown, not offered as a choice. */
29
- | { of: 'expression' }
21
+ /** What one dimension of a part is worked out from (figure-model's `V3Follow`). */
22
+ export type Follow = V3Follow
30
23
 
31
24
  /**
32
25
  * `link-dimension` routes a part's dimension through `<part>.dimension.<axis>` and points its first argument at the
33
26
  * source, directly or through the nodes that scale it. Walking back through those nodes finds the source: the
34
27
  * instance size (`size.<axis>`), another part's dimension (`<part>.dimension.<axis>.value`) or a declared input.
35
28
  */
29
+ /** What one measure of a part is worked out from, read back from the graph (figure-model's reader, one copy). */
36
30
  export function followOf(asset: V3Asset, part: string, axis: Axis): Follow {
37
- const m = modelOf(asset)
38
- /*
39
- A link routes the dimension through `<part>.dimension.<axis>`; before any link the shape reads its measure directly,
40
- and that may already be worked out (ROBOT_ARM's flange fills what is left).
41
- */
42
- const node = m.nodes.find(n => n.id === `${part}.dimension.${axis}`)
43
- const start = node ? node.args[0] : m.nodes.find(n => n.id === `${part}.shape`)?.args[AXES.indexOf(axis)]
44
- if (!start || m.constants.some(c => c.id === start)) return { of: 'own' }
45
- const writerOf = (ref: string) => m.nodes.find(n => Object.values(n.outputs).includes(ref))
46
- const seen = new Set<string>()
47
- const constant = (ref: string) => m.constants.find(c => c.id === ref)
48
- /* The constant factors met on the way back: a share of the instance size is `size × share`. */
49
- let times = 1
50
- /*
51
- A link the author made is `source × times + plus` (link-dimension): its outermost sum with a number is the plus --
52
- a leaf the opening's width less its gaps. Deeper sums, or a measure no link routes, read as worked out elsewhere.
53
- */
54
- let plus = 0
55
- let at = start
56
- const outer = node && writerOf(start)
57
- if (outer?.op === 'add@1') {
58
- const numbers = outer.args.filter(arg => constant(arg))
59
- const onward = outer.args.filter(arg => !constant(arg))
60
- if (numbers.length === 1 && onward.length === 1) {
61
- plus = constant(numbers[0]!)!.value
62
- at = onward[0]!
63
- }
64
- }
65
- const withPlus = <T extends object>(f: T) => (plus ? { ...f, plus } : f)
66
- const walk = (ref: string): Follow => {
67
- if (seen.has(ref)) return { of: 'expression' }
68
- seen.add(ref)
69
- const instance = /^size\.([xyz])$/.exec(ref)
70
- if (instance) return withPlus({ of: 'instance' as const, axis: instance[1] as Axis, times: Math.round(times * 1e6) / 1e6 })
71
- const other = /^(.+)\.dimension\.([xyz])\.value$/.exec(ref)
72
- if (other) return withPlus({ of: 'part' as const, part: other[1]!, axis: other[2] as Axis, ...(times !== 1 ? { times: Math.round(times * 1e6) / 1e6 } : {}) })
73
- const input = m.inputs.find(i => i.id === ref)
74
- if (input) return ref.startsWith(`${part}.`) ? { of: 'own' } : { of: 'input', input: ref }
75
- const writer = writerOf(ref)
76
- if (!writer) return { of: 'expression' }
77
- /* Follow the one argument that is not a constant factor. */
78
- const onward = writer.args.filter(arg => !constant(arg))
79
- if (onward.length !== 1) return { of: 'expression' }
80
- /*
81
- Only a product is a share of its source. A sum with a number is not (「fill what is left」 is the size plus a negative
82
- gap -- ROBOT_ARM's tool flange), nor anything else; those read as worked out some other way.
83
- */
84
- const factors = writer.args.filter(arg => constant(arg)).map(arg => constant(arg)!.value)
85
- if (writer.op === 'mul@1') times *= factors.reduce((a, b) => a * b, 1)
86
- else if (writer.op === 'div@1' && writer.args[0] === onward[0] && factors.length === 1 && factors[0] !== 0) times /= factors[0]!
87
- else if (factors.some(v => v !== 0) || !['mul@1', 'div@1', 'add@1', 'sub@1'].includes(writer.op)) return { of: 'expression' }
88
- return walk(onward[0]!)
89
- }
90
- return walk(at)
31
+ return v3FollowOf(asset, part, axis)
91
32
  }
92
33
 
93
34
  /** Whether the asset declares an instance size on an axis — only then can a dimension follow it. */
@@ -123,7 +64,7 @@ export function followableAxes(asset: V3Asset, part: string): Axis[] {
123
64
  const shape = modelOf(asset).nodes.find(n => n.id === `${part}.shape`)
124
65
  if (!shape) return []
125
66
  /* A door frame's first three measures are its width, height and depth, as a box's (figure-model portal-profile@2). */
126
- if (shape.op === 'box-shape@1' || shape.op === 'rounded-box@1' || shape.op === 'portal-profile@2') return [...AXES]
67
+ if (shape.op === 'box-shape@1' || shape.op === 'rounded-box@1' || shape.op === 'portal-profile@2' || shape.op === 'hollow-box@2') return [...AXES]
127
68
  if (shape.op === 'cylinder-shape@1') return ['x', 'y']
128
69
  return []
129
70
  }
@@ -34,6 +34,9 @@ export function paletteRecipe(kind: V3PaletteKind, id: string): V3PrimitiveCreat
34
34
  return { ...standing(10), kind: 'box', dimensions: { width: 600, height: 10, depth: 400 } }
35
35
  case 'wall':
36
36
  return { ...standing(2400), kind: 'box', dimensions: { width: 1000, height: 2400, depth: 100 } }
37
+ case 'hollow-box':
38
+ /* A tote at a tote's size: walls and floor that keep their thickness when it grows. */
39
+ return { ...standing(300), kind: 'hollow-box', dimensions: { width: 600, height: 300, depth: 400, roundX: 20, roundZ: 20, wallX: 10, wallZ: 10, floor: 15 } }
37
40
  case 'portal':
38
41
  return {
39
42
  id,
@@ -0,0 +1,15 @@
1
+ import { fetchRecipeGuide, fetchRecipeTemplates } from '../graphql/index.js'
2
+ import { templatesOf, type RecipeTemplate } from './v3-recipes.js'
3
+
4
+ /**
5
+ * Where the modeller gets its recipe templates: the server's static files (ADR-0095, amended 2026-09-26). The one place
6
+ * they are read -- the rules in `v3-recipes.ts` stay free of the network so they are tested without it.
7
+ */
8
+ export async function availableRecipes(lang: string): Promise<RecipeTemplate[]> {
9
+ return templatesOf(await fetchRecipeTemplates(lang))
10
+ }
11
+
12
+ /** One template's guide, or '' while none is written -- the screen then shows no guide and no word about one. */
13
+ export function recipeGuide(name: string, lang: string): Promise<string> {
14
+ return fetchRecipeGuide(name, lang)
15
+ }
@@ -1,22 +1,24 @@
1
+ import { describeV3Recipe } from '@hatiolab/figure-model'
1
2
  import type { V3Recipe, V3RecipeParam, V3RecipeValue } from '@hatiolab/figure-model'
2
3
 
4
+ import type { RecipeTemplateRow } from '../graphql/index.js'
5
+
6
+ /** A template in the catalog: the server's row, its recipe parsed. */
7
+ export interface RecipeTemplate extends Omit<RecipeTemplateRow, 'source'> {
8
+ recipe: V3Recipe
9
+ }
10
+
3
11
  /*
4
12
  * The recipe section's rules (ADR-0095), kept apart from the element so they can be tested without a browser.
5
13
  */
6
14
 
7
- /** A recipe's words in the language on screen: that language, else English, else the first it has. */
8
- export function recipeWord(words: Record<string, string> | undefined, language: string): string {
9
- if (!words) return ''
10
- return words[language] ?? words[language.split('-')[0]!] ?? words.en ?? Object.values(words)[0] ?? ''
11
- }
12
-
13
- /** The recipes as the server gave them, parsed; a row that is not a recipe is left out rather than breaking the list. */
14
- export function recipesOf(rows: ReadonlyArray<{ source: string }>): V3Recipe[] {
15
- const out: V3Recipe[] = []
16
- for (const row of rows) {
15
+ /** The server's rows as templates. A row that is not a recipe is left out rather than breaking the catalog. */
16
+ export function templatesOf(rows: ReadonlyArray<RecipeTemplateRow>): RecipeTemplate[] {
17
+ const out: RecipeTemplate[] = []
18
+ for (const { source, ...row } of rows) {
17
19
  try {
18
- const recipe = JSON.parse(row.source)
19
- if (recipe && typeof recipe.name === 'string' && Array.isArray(recipe.steps)) out.push(recipe)
20
+ const recipe = JSON.parse(source)
21
+ if (recipe && typeof recipe.name === 'string' && Array.isArray(recipe.steps)) out.push({ ...row, recipe })
20
22
  } catch {
21
23
  /* not a recipe */
22
24
  }
@@ -24,6 +26,34 @@ export function recipesOf(rows: ReadonlyArray<{ source: string }>): V3Recipe[] {
24
26
  return out
25
27
  }
26
28
 
29
+ /** A part the recipe makes, as the catalog names it: its own name, the placeholder left off. */
30
+ export const recipePartName = (part: string) => part.replace(/^\{id\}-/, '')
31
+
32
+ /**
33
+ * How a template works, in lines the catalog shows -- worked out from its steps (figure-model `describeV3Recipe`), each
34
+ * a translation key and its words.
35
+ */
36
+ export function recipeWorkings(recipe: V3Recipe): Array<{ key: string; words: Record<string, string | number> }> {
37
+ const d = describeV3Recipe(recipe)
38
+ const lines: Array<{ key: string; words: Record<string, string | number> }> = []
39
+ for (const m of d.moves)
40
+ lines.push(
41
+ m.follows
42
+ ? { key: 'figure.text.recipe-moves-with', words: { part: recipePartName(m.part), leader: recipePartName(m.follows) } }
43
+ : { key: m.kind === 'slide' ? 'figure.text.recipe-slides' : 'figure.text.recipe-turns', words: { part: recipePartName(m.part) } }
44
+ )
45
+ if (d.controls.length) lines.push({ key: 'figure.text.recipe-controls', words: { count: d.controls.length } })
46
+ if (d.shownByControl.length) lines.push({ key: 'figure.text.recipe-shown-by-control', words: { parts: d.shownByControl.map(recipePartName).join(', ') } })
47
+ for (const row of d.rows) lines.push({ key: 'figure.text.recipe-row', words: { part: recipePartName(row) } })
48
+ return lines
49
+ }
50
+
51
+ /** A recipe's words in the language on screen: that language, else English, else the first it has. */
52
+ export function recipeWord(words: Record<string, string> | undefined, language: string): string {
53
+ if (!words) return ''
54
+ return words[language] ?? words[language.split('-')[0]!] ?? words.en ?? Object.values(words)[0] ?? ''
55
+ }
56
+
27
57
  /** Whether a recipe can be put in now: one for a part needs a part chosen. */
28
58
  export function recipeReady(recipe: Pick<V3Recipe, 'target'>, selected: string): boolean {
29
59
  return recipe.target === 'figure' || !!selected
@@ -49,3 +79,14 @@ export function recipeFieldOf(param: V3RecipeParam, value: V3RecipeValue): strin
49
79
  if (param.kind === 'ratio' && typeof value === 'number') return String(Math.round(value * 1e6) / 1e4)
50
80
  return String(value)
51
81
  }
82
+
83
+
84
+ /** The templates a category and words find: a word matches the title, the description, the name or a tag. */
85
+ export function recipesMatching<T extends { category: string; title: string; description: string; name: string; tags: string[] }>(templates: readonly T[], category: string, query: string): T[] {
86
+ const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean)
87
+ return templates.filter(t => {
88
+ if (category && t.category !== category) return false
89
+ const text = [t.title, t.description, t.name, ...t.tags].join(' ').toLowerCase()
90
+ return words.every(w => text.includes(w))
91
+ })
92
+ }
@@ -1,13 +1,14 @@
1
1
  import { html, css } from 'lit'
2
2
  import { customElement, state } from 'lit/decorators.js'
3
3
  import { navigate, PageView } from '@operato/shell'
4
+ import { getPathInfo } from '@operato/utils'
4
5
  import { modellerPathOf } from '../modeller/modeller-route.js'
5
6
  import { createV3ProductSession } from '../modeller/v3-product-session.js'
6
7
  import { v3ProductStore } from '../graphql/v3-store.js'
7
8
  import { i18next } from '@operato/i18n'
8
- import { fetchFigure, fetchFigureRecipes, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
9
- import { recipesOf } from '../modeller/v3-recipes.js'
10
- import type { V3Recipe } from '@hatiolab/figure-model'
9
+ import { fetchFigure, fetchFigureTypeNames, updateFigure } from '../graphql/index.js'
10
+ import { availableRecipes, recipeGuide } from '../modeller/v3-recipe-source.js'
11
+ import type { RecipeTemplate } from '../modeller/v3-recipes.js'
11
12
  import { occupancyFromParts } from '../modeller/v3-occupancy.js'
12
13
  import type { V3AuthoringAction } from '@hatiolab/figure-model'
13
14
  import '../modeller/v3-editor.js'
@@ -95,9 +96,12 @@ export class FigureV3ModellerPage extends PageView {
95
96
  private routeId: string | undefined
96
97
  private discardApproved = false
97
98
  @state() private revision = 0
98
- /** The recipes this domain sees (ADR-0095), read once when the page is first shown. */
99
- @state() private recipes: V3Recipe[] = []
100
- private recipesRead = false
99
+ /** The recipe templates (ADR-0095), read once when the page is first shown; none read, and the section is not drawn. */
100
+ @state() private templates: RecipeTemplate[] = []
101
+ private templatesRead = false
102
+ private guideOf = (name: string) => recipeGuide(name, i18next.language || 'en')
103
+ /* The domain part of the address is read the way the shell reads it (figure-thumb does the same). */
104
+ private thumbnailOf = (name: string) => `${getPathInfo(location.pathname).contextPath}/figure-recipe-thumbnail/${name}`
101
105
  @state() private error = ''
102
106
  /**
103
107
  * 발행 패널이 읽는 행의 사실 — 상태와 발행 판 번호. 원본은 세션이 들고, 이 둘은 행에 있다.
@@ -376,12 +380,11 @@ export class FigureV3ModellerPage extends PageView {
376
380
  this.withdrawAssistant()
377
381
  return
378
382
  }
379
- if (!this.recipesRead) {
380
- this.recipesRead = true
381
- /* A recipe list that cannot be read leaves the section out; the modeller works without it. */
382
- fetchFigureRecipes()
383
- .then(rows => (this.recipes = recipesOf(rows)))
384
- .catch(() => (this.recipesRead = false))
383
+ if (!this.templatesRead) {
384
+ this.templatesRead = true
385
+ availableRecipes(i18next.language || 'en')
386
+ .then(templates => (this.templates = templates))
387
+ .catch(() => (this.templatesRead = false))
385
388
  }
386
389
  const id = lifecycle?.resourceId
387
390
  if (this.session.read().context && id === this.routeId && !changes.active) return
@@ -447,7 +450,7 @@ export class FigureV3ModellerPage extends PageView {
447
450
  render() {
448
451
  if (this.error) return html`<p role="alert" class="error">${this.error}</p>`
449
452
  const view = this.session.read()
450
- return html`<figure-v3-editor .session=${this.session} .recipes=${this.recipes} .firstSave=${this.askFirstSave} .revision=${this.revision} .instances=${this.instances} .budget=${this.budget} @v3-changed=${this.changed}>
453
+ return html`<figure-v3-editor .session=${this.session} .templates=${this.templates} .guideOf=${this.guideOf} .thumbnailOf=${this.thumbnailOf} .firstSave=${this.askFirstSave} .revision=${this.revision} .instances=${this.instances} .budget=${this.budget} @v3-changed=${this.changed}>
451
454
  ${this.commandError ? html`<p slot="notice" fit-error role="alert">${this.commandError}</p>` : ''}
452
455
  <figure-v3-properties
453
456
  slot="figure"
@@ -65,8 +65,17 @@ export declare function inspectFigureDraft(source: string, id?: string): Promise
65
65
  */
66
66
  export declare function fetchReleasedSource(id: string): Promise<string | undefined>;
67
67
  export declare function fetchFigureVersions(id: string): Promise<FigureVersion[]>;
68
- /** The recipes this domain sees (ADR-0095): released ones, its own and its parent's. Each is a figure-model V3Recipe. */
69
- export declare function fetchFigureRecipes(): Promise<Array<{
68
+ /** A recipe template as the server gives it (ADR-0095, amended): words in the language asked, the recipe as JSON. */
69
+ export interface RecipeTemplateRow {
70
70
  name: string;
71
+ category: string;
72
+ tags: string[];
73
+ target: 'part' | 'figure';
74
+ title: string;
75
+ description: string;
71
76
  source: string;
72
- }>>;
77
+ hasThumbnail: boolean;
78
+ }
79
+ export declare function fetchRecipeTemplates(lang: string): Promise<RecipeTemplateRow[]>;
80
+ /** One template's guide in the language asked, or '' while none is written. */
81
+ export declare function fetchRecipeGuide(name: string, lang: string): Promise<string>;
@@ -283,19 +283,38 @@ export async function fetchFigureVersions(id) {
283
283
  });
284
284
  return unwrap(response, 'figureVersions') ?? [];
285
285
  }
286
- /** The recipes this domain sees (ADR-0095): released ones, its own and its parent's. Each is a figure-model V3Recipe. */
287
- export async function fetchFigureRecipes() {
286
+ export async function fetchRecipeTemplates(lang) {
288
287
  const response = await client.query({
289
288
  query: gql `
290
- query {
291
- figureRecipes {
289
+ query ($lang: String) {
290
+ figureRecipeTemplates(lang: $lang) {
292
291
  name
292
+ category
293
+ tags
294
+ target
295
+ title
296
+ description
293
297
  source
298
+ hasThumbnail
294
299
  }
295
300
  }
296
301
  `,
297
- fetchPolicy: 'network-only'
302
+ variables: { lang }
303
+ });
304
+ return unwrap(response, 'figureRecipeTemplates') ?? [];
305
+ }
306
+ /** One template's guide in the language asked, or '' while none is written. */
307
+ export async function fetchRecipeGuide(name, lang) {
308
+ const response = await client.query({
309
+ query: gql `
310
+ query ($name: String!, $lang: String) {
311
+ figureRecipeTemplate(name: $name, lang: $lang) {
312
+ guide
313
+ }
314
+ }
315
+ `,
316
+ variables: { name, lang }
298
317
  });
299
- return unwrap(response, 'figureRecipes') ?? [];
318
+ return unwrap(response, 'figureRecipeTemplate')?.guide ?? '';
300
319
  }
301
320
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAYzC;;;;;;;;;;GAUG;AACH,SAAS,MAAM,CAAI,QAAqF,EAAE,KAAa;IACrH,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAA;IAC1C,IAAI,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,CAAA;IAE/B,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,oGAAoG;QACpG,OAAO,CAAC,KAAK,CAAC,+CAA+C,KAAK,EAAE,CAAC,CAAA;QACrE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAA;IACnE,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAM,CAAA;AAClC,CAAC;AAGD;;;;;;;GAOG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;CAe1B,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAKrC;IACC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;oBAGM,kBAAkB;;;;KAIjC;QACD,SAAS,EAAE;YACT,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE;YAC7B,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE;YACjE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;SAC1F;QACD,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAmB,QAAQ,EAAE,SAAS,CAAC,CAAA;AACtD,CAAC;AAED,qCAAqC;AACrC,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAU;IAC1C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;YAGF,kBAAkB;;;;;KAKzB;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAS,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;KAIT;QACD,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAkB,QAAQ,EAAE,iBAAiB,CAAC,IAAI,EAAE,CAAA;AACnE,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAiB;IAClD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;qBAGI,kBAAkB;;;;KAIlC;QACD,SAAS,EAAE,EAAE,MAAM,EAAE;KACtB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAa,QAAQ,EAAE,cAAc,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAU,EAAE,KAAkB;IAC/D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;qBAGI,kBAAkB;;;;KAIlC;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE;KACzB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAa,QAAQ,EAAE,cAAc,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EAAU;IAC3C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;;KAIZ;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;KAClB,CAAC,CAAA;IAEF,OAAO,MAAM,CAAU,QAAQ,EAAE,cAAc,CAAC,CAAA;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAU,EAAE,OAAgB;IAC9D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;KASZ;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;KAC3B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAS,QAAQ,EAAE,eAAe,CAAC,CAAA;AAClD,CAAC;AAED,kDAAkD;AAClD,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAU,EAAE,OAAe;IACnE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,GAAG,CAAA;;;qBAGI,kBAAkB;;;;KAIlC;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;KAC3B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAa,QAAQ,EAAE,qBAAqB,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAU;IAC5C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;KAcT;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAmB,QAAQ,EAAE,eAAe,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc,EAAE,EAAW;IAClE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;;KAcT;QACD,SAAS,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;QACzB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAmB,QAAQ,EAAE,oBAAoB,CAAC,CAAA;AACjE,CAAC;AAED,2BAA2B;AAC3B;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAU;IAClD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;KAOT;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAsB,QAAQ,EAAE,gBAAgB,CAAC,EAAE,MAAM,IAAI,SAAS,CAAA;AACrF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,EAAU;IAClD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;;;;;KAaT;QACD,SAAS,EAAE,EAAE,EAAE,EAAE;QACjB,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IAEF,OAAO,MAAM,CAAkB,QAAQ,EAAE,gBAAgB,CAAC,IAAI,EAAE,CAAA;AAClE,CAAC;AAED,yHAAyH;AACzH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;QAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;KAOT;QACD,WAAW,EAAE,cAAc;KAC5B,CAAC,CAAA;IACF,OAAO,MAAM,CAAiD,QAAQ,EAAE,eAAe,CAAC,IAAI,EAAE,CAAA;AAChG,CAAC","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\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 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/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\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 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<{ source?: string }>(response, 'figureReleased')?.source ?? 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/** The recipes this domain sees (ADR-0095): released ones, its own and its parent's. Each is a figure-model V3Recipe. */\nexport async function fetchFigureRecipes(): Promise<Array<{ name: string; source: string }>> {\n const response = await client.query({\n query: gql`\n query {\n figureRecipes {\n name\n source\n }\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<Array<{ name: string; source: string }> | null>(response, 'figureRecipes') ?? []\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 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 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/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\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 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<{ source?: string }>(response, 'figureReleased')?.source ?? 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"]}
@@ -1,6 +1,6 @@
1
1
  import '@material/web/icon/icon.js';
2
2
  import { LitElement } from 'lit';
3
- import type { V3PrimitiveCreation, V3Recipe } from '@hatiolab/figure-model';
3
+ import type { V3PrimitiveCreation } from '@hatiolab/figure-model';
4
4
  import type { V3BudgetLine } from './v3-maturity.js';
5
5
  /**
6
6
  * 도형 팔레트와 부품 목록 — V3 자산의 것.
@@ -47,13 +47,8 @@ export declare class FigureV3Parts extends FigureV3Parts_base {
47
47
  used: number;
48
48
  limit: number | null;
49
49
  };
50
- /** The recipes this domain sees (ADR-0095). */
51
- recipes: V3Recipe[];
52
- /** Why the last recipe put in was refused -- the kernel's sentence, as it is (ADR-0095 condition 3). */
53
- recipeError: string;
54
- /** The recipe whose form is open, and the values in it. */
55
- private openRecipe;
56
- private recipeValues;
50
+ /** How many recipe templates the catalog holds; none, and the section is not drawn. */
51
+ recipeCount: number;
57
52
  render(): import("lit-html").TemplateResult<1>;
58
53
  /** 디테일 등급 — 고르면 한도가 바뀐다. 목록 머리 아래. */
59
54
  private renderLevel;
@@ -64,12 +59,10 @@ export declare class FigureV3Parts extends FigureV3Parts_base {
64
59
  /** 고른 줄의 조작. 못 하는 것은 빼지 않고 흐리게 둔다 — 자리가 밀리면 연달아 지우다 엉뚱한 것을 누른다. */
65
60
  private renderOps;
66
61
  /**
67
- * Recipes (ADR-0095): a door, a roller row -- parts made in one step, with a few numbers and choices. A recipe for a
68
- * part needs one chosen; its form opens under it, and putting it in is one step of the history.
62
+ * Recipes (ADR-0095): the catalog of templates -- a door, a roller row -- opened over the modeller, put onto the chosen
63
+ * part in one step.
69
64
  */
70
65
  private renderRecipes;
71
- private openFor;
72
- private renderRecipeForm;
73
66
  /** 정본을 고치지 않는다 — 무엇을 하겠다는 것만 알린다. */
74
67
  private ask;
75
68
  }