@things-factory/figure-ui 10.1.68 → 10.1.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/graphql/index.ts +89 -0
- package/client/modeller/figure-v3-links.ts +73 -7
- package/client/modeller/figure-v3-parts.ts +153 -2
- package/client/modeller/v3-editor.ts +133 -5
- package/client/modeller/v3-gizmo.ts +18 -2
- package/client/modeller/v3-links.ts +71 -2
- package/client/modeller/v3-palette.ts +2 -1
- package/client/modeller/v3-product-session.ts +8 -0
- package/client/modeller/v3-recipes.ts +69 -0
- package/client/pages/figure-recipe-page.ts +338 -0
- package/client/pages/figure-v3-modeller-page.ts +25 -2
- package/client/route.ts +4 -0
- package/dist-client/graphql/index.d.ts +21 -0
- package/dist-client/graphql/index.js +75 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-v3-capability.d.ts +1 -1
- package/dist-client/modeller/figure-v3-clips.d.ts +1 -1
- package/dist-client/modeller/figure-v3-controls.d.ts +1 -1
- package/dist-client/modeller/figure-v3-links.d.ts +11 -1
- package/dist-client/modeller/figure-v3-links.js +69 -8
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +15 -1
- package/dist-client/modeller/figure-v3-parts.js +166 -1
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
- package/dist-client/modeller/v3-clips.d.ts +1 -1
- package/dist-client/modeller/v3-editor.d.ts +26 -0
- package/dist-client/modeller/v3-editor.js +147 -4
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-gizmo.d.ts +6 -0
- package/dist-client/modeller/v3-gizmo.js +20 -2
- package/dist-client/modeller/v3-gizmo.js.map +1 -1
- package/dist-client/modeller/v3-links.d.ts +27 -0
- package/dist-client/modeller/v3-links.js +68 -2
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-palette.js +2 -1
- package/dist-client/modeller/v3-palette.js.map +1 -1
- package/dist-client/modeller/v3-product-session.d.ts +34 -0
- package/dist-client/modeller/v3-product-session.js +8 -0
- package/dist-client/modeller/v3-product-session.js.map +1 -1
- package/dist-client/modeller/v3-recipes.d.ts +26 -0
- package/dist-client/modeller/v3-recipes.js +69 -0
- package/dist-client/modeller/v3-recipes.js.map +1 -0
- package/dist-client/pages/figure-recipe-page.d.ts +41 -0
- package/dist-client/pages/figure-recipe-page.js +352 -0
- package/dist-client/pages/figure-recipe-page.js.map +1 -0
- package/dist-client/pages/figure-v3-modeller-page.d.ts +4 -0
- package/dist-client/pages/figure-v3-modeller-page.js +29 -2
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.fui.tsbuildinfo +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/modeller-page.js +15 -1
- package/test/browser/modeller.check.mjs +217 -2
- package/test/modeller-route.test.ts +9 -0
- package/test/v3-gizmo.test.ts +44 -0
- package/test/v3-links.test.ts +91 -1
- package/test/v3-recipes.test.ts +60 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +40 -8
- package/translations/ja.json +40 -8
- package/translations/ko.json +40 -8
- package/translations/ms.json +40 -8
- package/translations/zh.json +40 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"v3-product-session.js","sourceRoot":"","sources":["../../client/modeller/v3-product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createV3Asset,\n createV3EditorWorkspace,\n serializeV3Asset,\n parseV3Asset,\n type V3InputChange,\n type V3PrimitiveCreation,\n type V3PartEdit,\n type V3AuthoringAction,\n type V3ProposalStep\n} from '@hatiolab/figure-model'\n\nexport interface V3ProductRecord {\n id: string\n source: string\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface V3ProductStore {\n load(id: string): Promise<V3ProductRecord>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n }): Promise<V3ProductRecord>\n}\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createV3ProductSession(store: V3ProductStore) {\n const workspace = createV3EditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n function read() {\n return { ...workspace.read(), id, loading, saving, error }\n }\n function writable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if (workspace.read().dirty && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n startNew({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeV3Asset(createV3Asset('새 Figure')))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: V3InputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw Error('AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly V3ProposalStep[], expectedRevision: number, context: string) {\n writable()\n return workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps })\n },\n add(part: V3PrimitiveCreation) {\n return workspace.addPrimitive({ ...writable(), part })\n },\n edit(edit: V3PartEdit) {\n return workspace.editPart({ ...writable(), edit })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: V3AuthoringAction) {\n return workspace.command({ ...writable(), action })\n },\n undo() {\n return workspace.undo(writable())\n },\n redo() {\n return workspace.redo(writable())\n },\n source() {\n const { context } = writable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `v3-thumbnail.ts`).\n */\n async save({ thumbnail, type }: { thumbnail?: string; type?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n saving = true\n try {\n const record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, ...(id || !type ? {} : { type }) })\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeV3Asset(parseV3Asset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n workspace.acknowledgeSaved(exported)\n id = record.id\n baseline = record.source\n } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\n }\n }\n}\nexport type V3ProductSession = ReturnType<typeof createV3ProductSession>\n"]}
|
|
1
|
+
{"version":3,"file":"v3-product-session.js","sourceRoot":"","sources":["../../client/modeller/v3-product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createV3Asset,\n createV3EditorWorkspace,\n serializeV3Asset,\n parseV3Asset,\n type V3InputChange,\n type V3PrimitiveCreation,\n type V3PartEdit,\n type V3AuthoringAction,\n type V3ProposalStep\n} from '@hatiolab/figure-model'\n\nexport interface V3ProductRecord {\n id: string\n source: string\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface V3ProductStore {\n load(id: string): Promise<V3ProductRecord>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n }): Promise<V3ProductRecord>\n}\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createV3ProductSession(store: V3ProductStore) {\n const workspace = createV3EditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n function read() {\n return { ...workspace.read(), id, loading, saving, error }\n }\n function writable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw Error('먼저 모델을 정상적으로 열어 주세요.')\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw Error('저장이 끝난 뒤 편집해 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if (workspace.read().dirty && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n startNew({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeV3Asset(createV3Asset('새 Figure')))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: V3InputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw Error('AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly V3ProposalStep[], expectedRevision: number, context: string) {\n writable()\n return workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps })\n },\n /**\n * A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open\n * now. The same path as a proposal; the person is its origin.\n */\n applyRecipe(steps: readonly V3ProposalStep[]) {\n const at = writable()\n return workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps })\n },\n add(part: V3PrimitiveCreation) {\n return workspace.addPrimitive({ ...writable(), part })\n },\n edit(edit: V3PartEdit) {\n return workspace.editPart({ ...writable(), edit })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: V3AuthoringAction) {\n return workspace.command({ ...writable(), action })\n },\n undo() {\n return workspace.undo(writable())\n },\n redo() {\n return workspace.redo(writable())\n },\n source() {\n const { context } = writable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `v3-thumbnail.ts`).\n */\n async save({ thumbnail, type }: { thumbnail?: string; type?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n saving = true\n try {\n const record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, ...(id || !type ? {} : { type }) })\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeV3Asset(parseV3Asset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n workspace.acknowledgeSaved(exported)\n id = record.id\n baseline = record.source\n } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\n }\n }\n}\nexport type V3ProductSession = ReturnType<typeof createV3ProductSession>\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { V3Recipe, V3RecipeParam, V3RecipeValue } from '@hatiolab/figure-model';
|
|
2
|
+
/** A recipe's words in the language on screen: that language, else English, else the first it has. */
|
|
3
|
+
export declare function recipeWord(words: Record<string, string> | undefined, language: string): string;
|
|
4
|
+
/** The recipes as the server gave them, parsed; a row that is not a recipe is left out rather than breaking the list. */
|
|
5
|
+
export declare function recipesOf(rows: ReadonlyArray<{
|
|
6
|
+
source: string;
|
|
7
|
+
}>): V3Recipe[];
|
|
8
|
+
/** Whether a recipe can be put in now: one for a part needs a part chosen. */
|
|
9
|
+
export declare function recipeReady(recipe: Pick<V3Recipe, 'target'>, selected: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* A field's value as the recipe takes it. A ratio is shown and typed as a percentage (35 for 0.35), since that is how a
|
|
12
|
+
* person says a share; the rest are taken as they are. A number field left empty or not a number keeps the default.
|
|
13
|
+
*/
|
|
14
|
+
export declare function recipeValueOf(param: V3RecipeParam, typed: string | boolean): V3RecipeValue;
|
|
15
|
+
/** What a field shows for a value: a ratio as a percentage. */
|
|
16
|
+
export declare function recipeFieldOf(param: V3RecipeParam, value: V3RecipeValue): string;
|
|
17
|
+
/** The domain on screen, from the address (`/domain/<subdomain>/…`); '' when the address names none. */
|
|
18
|
+
export declare function subdomainOf(pathname: string): string;
|
|
19
|
+
/** Whose a recipe row is: this domain's own (it may change it) or a parent's (copied in to be changed). */
|
|
20
|
+
export declare function recipeIsOwn(row: {
|
|
21
|
+
domain?: {
|
|
22
|
+
subdomain?: string;
|
|
23
|
+
};
|
|
24
|
+
}, subdomain: string): boolean;
|
|
25
|
+
/** What a parameter takes, in a few words: a range, a percentage range, or its options. */
|
|
26
|
+
export declare function paramRangeOf(param: V3RecipeParam): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The recipe section's rules (ADR-0095), kept apart from the element so they can be tested without a browser.
|
|
3
|
+
*/
|
|
4
|
+
/** A recipe's words in the language on screen: that language, else English, else the first it has. */
|
|
5
|
+
export function recipeWord(words, language) {
|
|
6
|
+
if (!words)
|
|
7
|
+
return '';
|
|
8
|
+
return words[language] ?? words[language.split('-')[0]] ?? words.en ?? Object.values(words)[0] ?? '';
|
|
9
|
+
}
|
|
10
|
+
/** The recipes as the server gave them, parsed; a row that is not a recipe is left out rather than breaking the list. */
|
|
11
|
+
export function recipesOf(rows) {
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const row of rows) {
|
|
14
|
+
try {
|
|
15
|
+
const recipe = JSON.parse(row.source);
|
|
16
|
+
if (recipe && typeof recipe.name === 'string' && Array.isArray(recipe.steps))
|
|
17
|
+
out.push(recipe);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* not a recipe */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/** Whether a recipe can be put in now: one for a part needs a part chosen. */
|
|
26
|
+
export function recipeReady(recipe, selected) {
|
|
27
|
+
return recipe.target === 'figure' || !!selected;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A field's value as the recipe takes it. A ratio is shown and typed as a percentage (35 for 0.35), since that is how a
|
|
31
|
+
* person says a share; the rest are taken as they are. A number field left empty or not a number keeps the default.
|
|
32
|
+
*/
|
|
33
|
+
export function recipeValueOf(param, typed) {
|
|
34
|
+
if (param.kind === 'flag')
|
|
35
|
+
return typed === true || typed === 'true';
|
|
36
|
+
if (param.kind === 'ratio' || param.kind === 'mm') {
|
|
37
|
+
const n = typeof typed === 'string' && typed.trim() !== '' ? Number(typed) : NaN;
|
|
38
|
+
if (!Number.isFinite(n))
|
|
39
|
+
return param.default;
|
|
40
|
+
return param.kind === 'ratio' ? Math.round(n * 1e4) / 1e6 : n;
|
|
41
|
+
}
|
|
42
|
+
const option = (param.options ?? []).find(o => String(o) === String(typed));
|
|
43
|
+
return option ?? param.default;
|
|
44
|
+
}
|
|
45
|
+
/** What a field shows for a value: a ratio as a percentage. */
|
|
46
|
+
export function recipeFieldOf(param, value) {
|
|
47
|
+
if (param.kind === 'ratio' && typeof value === 'number')
|
|
48
|
+
return String(Math.round(value * 1e6) / 1e4);
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
/** The domain on screen, from the address (`/domain/<subdomain>/…`); '' when the address names none. */
|
|
52
|
+
export function subdomainOf(pathname) {
|
|
53
|
+
return /^\/domain\/([^/]+)/.exec(pathname)?.[1] ?? '';
|
|
54
|
+
}
|
|
55
|
+
/** Whose a recipe row is: this domain's own (it may change it) or a parent's (copied in to be changed). */
|
|
56
|
+
export function recipeIsOwn(row, subdomain) {
|
|
57
|
+
return !!subdomain && row.domain?.subdomain === subdomain;
|
|
58
|
+
}
|
|
59
|
+
/** What a parameter takes, in a few words: a range, a percentage range, or its options. */
|
|
60
|
+
export function paramRangeOf(param) {
|
|
61
|
+
if (param.kind === 'ratio')
|
|
62
|
+
return `${recipeFieldOf(param, param.min ?? 0)} – ${recipeFieldOf(param, param.max ?? 1)} %`;
|
|
63
|
+
if (param.kind === 'mm')
|
|
64
|
+
return `${param.min ?? ''} – ${param.max ?? ''} mm`;
|
|
65
|
+
if (param.kind === 'flag')
|
|
66
|
+
return '';
|
|
67
|
+
return (param.options ?? []).join(' / ');
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=v3-recipes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-recipes.js","sourceRoot":"","sources":["../../client/modeller/v3-recipes.ts"],"names":[],"mappings":"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","sourcesContent":["import type { V3Recipe, V3RecipeParam, V3RecipeValue } from '@hatiolab/figure-model'\n\n/*\n * The recipe section's rules (ADR-0095), kept apart from the element so they can be tested without a browser.\n */\n\n/** A recipe's words in the language on screen: that language, else English, else the first it has. */\nexport function recipeWord(words: Record<string, string> | undefined, language: string): string {\n if (!words) return ''\n return words[language] ?? words[language.split('-')[0]!] ?? words.en ?? Object.values(words)[0] ?? ''\n}\n\n/** The recipes as the server gave them, parsed; a row that is not a recipe is left out rather than breaking the list. */\nexport function recipesOf(rows: ReadonlyArray<{ source: string }>): V3Recipe[] {\n const out: V3Recipe[] = []\n for (const row of rows) {\n try {\n const recipe = JSON.parse(row.source)\n if (recipe && typeof recipe.name === 'string' && Array.isArray(recipe.steps)) out.push(recipe)\n } catch {\n /* not a recipe */\n }\n }\n return out\n}\n\n/** Whether a recipe can be put in now: one for a part needs a part chosen. */\nexport function recipeReady(recipe: Pick<V3Recipe, 'target'>, selected: string): boolean {\n return recipe.target === 'figure' || !!selected\n}\n\n/**\n * A field's value as the recipe takes it. A ratio is shown and typed as a percentage (35 for 0.35), since that is how a\n * person says a share; the rest are taken as they are. A number field left empty or not a number keeps the default.\n */\nexport function recipeValueOf(param: V3RecipeParam, typed: string | boolean): V3RecipeValue {\n if (param.kind === 'flag') return typed === true || typed === 'true'\n if (param.kind === 'ratio' || param.kind === 'mm') {\n const n = typeof typed === 'string' && typed.trim() !== '' ? Number(typed) : NaN\n if (!Number.isFinite(n)) return param.default\n return param.kind === 'ratio' ? Math.round(n * 1e4) / 1e6 : n\n }\n const option = (param.options ?? []).find(o => String(o) === String(typed))\n return option ?? param.default\n}\n\n/** What a field shows for a value: a ratio as a percentage. */\nexport function recipeFieldOf(param: V3RecipeParam, value: V3RecipeValue): string {\n if (param.kind === 'ratio' && typeof value === 'number') return String(Math.round(value * 1e6) / 1e4)\n return String(value)\n}\n\n/** The domain on screen, from the address (`/domain/<subdomain>/…`); '' when the address names none. */\nexport function subdomainOf(pathname: string): string {\n return /^\\/domain\\/([^/]+)/.exec(pathname)?.[1] ?? ''\n}\n\n/** Whose a recipe row is: this domain's own (it may change it) or a parent's (copied in to be changed). */\nexport function recipeIsOwn(row: { domain?: { subdomain?: string } }, subdomain: string): boolean {\n return !!subdomain && row.domain?.subdomain === subdomain\n}\n\n/** What a parameter takes, in a few words: a range, a percentage range, or its options. */\nexport function paramRangeOf(param: V3RecipeParam): string {\n if (param.kind === 'ratio') return `${recipeFieldOf(param, param.min ?? 0)} – ${recipeFieldOf(param, param.max ?? 1)} %`\n if (param.kind === 'mm') return `${param.min ?? ''} – ${param.max ?? ''} mm`\n if (param.kind === 'flag') return ''\n return (param.options ?? []).join(' / ')\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import '@material/web/icon/icon.js';
|
|
2
|
+
import '../modeller/figure-v3-preview.js';
|
|
3
|
+
import { PageView } from '@operato/shell';
|
|
4
|
+
declare const FigureRecipePage_base: (new (...args: any[]) => import("lit").LitElement) & typeof PageView;
|
|
5
|
+
/**
|
|
6
|
+
* The recipes (ADR-0095): what each one puts in, shown put into a room at several sizes, and whose it is.
|
|
7
|
+
*
|
|
8
|
+
* A recipe of this domain's own can be released, taken back to draft, changed or deleted. A parent's -- the ones the
|
|
9
|
+
* system domain ships -- is copied in under its name to be changed, and the copy stands in for it. Figures a recipe was
|
|
10
|
+
* put into keep their parts whatever happens here.
|
|
11
|
+
*/
|
|
12
|
+
export declare class FigureRecipePage extends FigureRecipePage_base {
|
|
13
|
+
static styles: import("lit").CSSResult[];
|
|
14
|
+
private rows;
|
|
15
|
+
private chosen;
|
|
16
|
+
private error;
|
|
17
|
+
private editing;
|
|
18
|
+
private text;
|
|
19
|
+
get context(): {
|
|
20
|
+
title: string;
|
|
21
|
+
};
|
|
22
|
+
pageUpdated(_changes: Record<string, unknown>, _lifecycle: unknown, before: {
|
|
23
|
+
active?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
private read;
|
|
26
|
+
private get subdomain();
|
|
27
|
+
render(): import("lit-html").TemplateResult<1>;
|
|
28
|
+
private renderRecipe;
|
|
29
|
+
/** The recipe put into the room it is checked in, with its defaults: what it makes, at several sizes. */
|
|
30
|
+
private renderPreview;
|
|
31
|
+
private choose;
|
|
32
|
+
private edit;
|
|
33
|
+
private run;
|
|
34
|
+
private setState;
|
|
35
|
+
private deleteRecipe;
|
|
36
|
+
/** A parent's recipe copied into this domain as a draft, to be changed here. */
|
|
37
|
+
private copyIn;
|
|
38
|
+
/** The source as typed: checked by the server in the test room; a new name is a new recipe. */
|
|
39
|
+
private saveText;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
3
|
+
import '../modeller/figure-v3-preview.js';
|
|
4
|
+
import { css, html, nothing } from 'lit';
|
|
5
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
6
|
+
import { i18next, localize } from '@operato/i18n';
|
|
7
|
+
import { PageView } from '@operato/shell';
|
|
8
|
+
import { OxPrompt } from '@operato/popup';
|
|
9
|
+
import { ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles';
|
|
10
|
+
import { applyV3Recipe, parseV3Recipe, v3RecipeTestRoom } from '@hatiolab/figure-model';
|
|
11
|
+
import { deleteFigureRecipe, fetchFigureRecipeRows, saveFigureRecipe, setFigureRecipeState } from '../graphql/index.js';
|
|
12
|
+
import { paramRangeOf, recipeFieldOf, recipeIsOwn, recipesOf, recipeWord, subdomainOf } from '../modeller/v3-recipes.js';
|
|
13
|
+
/**
|
|
14
|
+
* The recipes (ADR-0095): what each one puts in, shown put into a room at several sizes, and whose it is.
|
|
15
|
+
*
|
|
16
|
+
* A recipe of this domain's own can be released, taken back to draft, changed or deleted. A parent's -- the ones the
|
|
17
|
+
* system domain ships -- is copied in under its name to be changed, and the copy stands in for it. Figures a recipe was
|
|
18
|
+
* put into keep their parts whatever happens here.
|
|
19
|
+
*/
|
|
20
|
+
let FigureRecipePage = class FigureRecipePage extends localize(i18next)(PageView) {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments);
|
|
23
|
+
this.rows = [];
|
|
24
|
+
this.chosen = '';
|
|
25
|
+
this.error = '';
|
|
26
|
+
this.editing = false;
|
|
27
|
+
this.text = '';
|
|
28
|
+
}
|
|
29
|
+
static { this.styles = [
|
|
30
|
+
WorkSurfacePageStyles,
|
|
31
|
+
ScrollbarStyles,
|
|
32
|
+
css `
|
|
33
|
+
:host {
|
|
34
|
+
display: grid;
|
|
35
|
+
grid-template-columns: minmax(220px, 280px) 1fr;
|
|
36
|
+
background-color: var(--md-sys-color-surface);
|
|
37
|
+
color: var(--md-sys-color-on-surface);
|
|
38
|
+
font-size: 0.85rem;
|
|
39
|
+
}
|
|
40
|
+
@container (max-width: 640px) {
|
|
41
|
+
:host {
|
|
42
|
+
grid-template-columns: 1fr;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
nav {
|
|
46
|
+
overflow: auto;
|
|
47
|
+
border-right: 1px solid var(--md-sys-color-outline-variant);
|
|
48
|
+
padding: 8px;
|
|
49
|
+
}
|
|
50
|
+
nav p[intro] {
|
|
51
|
+
margin: 4px 4px 10px;
|
|
52
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
53
|
+
line-height: 1.5;
|
|
54
|
+
}
|
|
55
|
+
button[row] {
|
|
56
|
+
display: grid;
|
|
57
|
+
gap: 2px;
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: 8px 10px;
|
|
60
|
+
text-align: left;
|
|
61
|
+
font: inherit;
|
|
62
|
+
color: inherit;
|
|
63
|
+
background: none;
|
|
64
|
+
border: none;
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
cursor: pointer;
|
|
67
|
+
}
|
|
68
|
+
button[row][on] {
|
|
69
|
+
background: var(--md-sys-color-secondary-container);
|
|
70
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
71
|
+
}
|
|
72
|
+
button[row] small {
|
|
73
|
+
display: flex;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
76
|
+
}
|
|
77
|
+
span[chip] {
|
|
78
|
+
padding: 0 6px;
|
|
79
|
+
border-radius: 999em;
|
|
80
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
81
|
+
font-size: 0.7rem;
|
|
82
|
+
}
|
|
83
|
+
span[chip][draft] {
|
|
84
|
+
color: var(--md-sys-color-tertiary);
|
|
85
|
+
border-color: var(--md-sys-color-tertiary);
|
|
86
|
+
}
|
|
87
|
+
main {
|
|
88
|
+
overflow: auto;
|
|
89
|
+
padding: 12px 16px;
|
|
90
|
+
display: grid;
|
|
91
|
+
gap: 12px;
|
|
92
|
+
align-content: start;
|
|
93
|
+
}
|
|
94
|
+
main h2 {
|
|
95
|
+
margin: 0;
|
|
96
|
+
font-size: var(--ui-t-title, 16px);
|
|
97
|
+
}
|
|
98
|
+
main p {
|
|
99
|
+
margin: 0;
|
|
100
|
+
line-height: 1.5;
|
|
101
|
+
}
|
|
102
|
+
p[muted] {
|
|
103
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
104
|
+
}
|
|
105
|
+
table {
|
|
106
|
+
border-collapse: collapse;
|
|
107
|
+
width: 100%;
|
|
108
|
+
max-width: 720px;
|
|
109
|
+
}
|
|
110
|
+
th,
|
|
111
|
+
td {
|
|
112
|
+
text-align: left;
|
|
113
|
+
padding: 4px 8px;
|
|
114
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
115
|
+
}
|
|
116
|
+
figure-v3-preview {
|
|
117
|
+
height: 420px;
|
|
118
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
119
|
+
border-radius: 8px;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
}
|
|
122
|
+
div[actions] {
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-wrap: wrap;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
}
|
|
127
|
+
div[actions] button {
|
|
128
|
+
display: inline-flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 4px;
|
|
131
|
+
padding: 6px 12px;
|
|
132
|
+
font: inherit;
|
|
133
|
+
color: var(--md-sys-color-on-surface);
|
|
134
|
+
background: none;
|
|
135
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
136
|
+
border-radius: 999em;
|
|
137
|
+
cursor: pointer;
|
|
138
|
+
}
|
|
139
|
+
div[actions] button[primary] {
|
|
140
|
+
color: var(--md-sys-color-on-primary);
|
|
141
|
+
background: var(--md-sys-color-primary);
|
|
142
|
+
border-color: transparent;
|
|
143
|
+
}
|
|
144
|
+
div[actions] button[danger] {
|
|
145
|
+
color: var(--md-sys-color-error);
|
|
146
|
+
border-color: var(--md-sys-color-error);
|
|
147
|
+
}
|
|
148
|
+
p[role='alert'] {
|
|
149
|
+
padding: 8px;
|
|
150
|
+
white-space: pre-wrap;
|
|
151
|
+
color: var(--md-sys-color-on-error-container);
|
|
152
|
+
background: var(--md-sys-color-error-container);
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
}
|
|
155
|
+
textarea {
|
|
156
|
+
width: 100%;
|
|
157
|
+
min-height: 320px;
|
|
158
|
+
font: 0.78rem/1.4 monospace;
|
|
159
|
+
box-sizing: border-box;
|
|
160
|
+
}
|
|
161
|
+
`
|
|
162
|
+
]; }
|
|
163
|
+
get context() {
|
|
164
|
+
return { title: i18next.t('figure.title.recipes') };
|
|
165
|
+
}
|
|
166
|
+
async pageUpdated(_changes, _lifecycle, before) {
|
|
167
|
+
if (this.active && !before?.active)
|
|
168
|
+
await this.read();
|
|
169
|
+
}
|
|
170
|
+
async read() {
|
|
171
|
+
try {
|
|
172
|
+
this.rows = await fetchFigureRecipeRows();
|
|
173
|
+
if (!this.rows.some(r => r.name === this.chosen))
|
|
174
|
+
this.chosen = this.rows[0]?.name ?? '';
|
|
175
|
+
this.error = '';
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
get subdomain() {
|
|
182
|
+
return subdomainOf(location.pathname);
|
|
183
|
+
}
|
|
184
|
+
render() {
|
|
185
|
+
const language = i18next.language || 'en';
|
|
186
|
+
const recipes = recipesOf(this.rows);
|
|
187
|
+
const row = this.rows.find(r => r.name === this.chosen);
|
|
188
|
+
const recipe = recipes.find(r => r.name === this.chosen);
|
|
189
|
+
return html `
|
|
190
|
+
<nav>
|
|
191
|
+
<p intro>${i18next.t('figure.text.recipes-intro')}</p>
|
|
192
|
+
${this.rows.map(one => {
|
|
193
|
+
const r = recipes.find(x => x.name === one.name);
|
|
194
|
+
return html `<button row ?on=${one.name === this.chosen} @click=${() => this.choose(one.name)}>
|
|
195
|
+
<span>${r ? recipeWord(r.title, language) : one.name}</span>
|
|
196
|
+
<small>
|
|
197
|
+
<span>${one.name}</span>
|
|
198
|
+
${one.state === 'draft' ? html `<span chip draft>${i18next.t('figure.label.recipe-draft')}</span>` : nothing}
|
|
199
|
+
${recipeIsOwn(one, this.subdomain) ? nothing : html `<span chip>${i18next.t('figure.label.recipe-inherited')}</span>`}
|
|
200
|
+
</small>
|
|
201
|
+
</button>`;
|
|
202
|
+
})}
|
|
203
|
+
</nav>
|
|
204
|
+
<main>
|
|
205
|
+
${this.error ? html `<p role="alert">${this.error}</p>` : nothing}
|
|
206
|
+
${row && recipe ? this.renderRecipe(row, recipe, language) : html `<p muted>${i18next.t('figure.text.recipes-empty')}</p>`}
|
|
207
|
+
</main>
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
renderRecipe(row, recipe, language) {
|
|
211
|
+
const own = recipeIsOwn(row, this.subdomain);
|
|
212
|
+
return html `
|
|
213
|
+
<h2>${recipeWord(recipe.title, language)}</h2>
|
|
214
|
+
<p>${recipeWord(recipe.description, language)}</p>
|
|
215
|
+
<p muted>
|
|
216
|
+
${i18next.t(recipe.target === 'part' ? 'figure.text.recipe-target-part' : 'figure.text.recipe-target-figure')} ·
|
|
217
|
+
${i18next.t(own ? 'figure.text.recipe-own' : 'figure.text.recipe-inherited', { domain: row.domain?.subdomain ?? '' })}
|
|
218
|
+
</p>
|
|
219
|
+
<table>
|
|
220
|
+
<thead>
|
|
221
|
+
<tr>
|
|
222
|
+
<th>${i18next.t('figure.label.recipe-param')}</th>
|
|
223
|
+
<th>${i18next.t('figure.label.recipe-default')}</th>
|
|
224
|
+
<th>${i18next.t('figure.label.recipe-range')}</th>
|
|
225
|
+
</tr>
|
|
226
|
+
</thead>
|
|
227
|
+
<tbody>
|
|
228
|
+
${recipe.params.map(p => html `<tr>
|
|
229
|
+
<td>${recipeWord(p.label, language) || p.name}</td>
|
|
230
|
+
<td>${p.kind === 'flag' ? i18next.t(p.default ? 'figure.label.yes' : 'figure.label.no') : recipeFieldOf(p, p.default)}${p.kind === 'ratio' ? ' %' : ''}</td>
|
|
231
|
+
<td>${paramRangeOf(p)}</td>
|
|
232
|
+
</tr>`)}
|
|
233
|
+
</tbody>
|
|
234
|
+
</table>
|
|
235
|
+
${this.renderPreview(recipe)}
|
|
236
|
+
<div actions>
|
|
237
|
+
${own
|
|
238
|
+
? html `${row.state === 'draft'
|
|
239
|
+
? html `<button primary @click=${() => this.setState(row.name, 'released')}>${i18next.t('figure.button.recipe-release')}</button>`
|
|
240
|
+
: html `<button @click=${() => this.setState(row.name, 'draft')}>${i18next.t('figure.button.recipe-to-draft')}</button>`}
|
|
241
|
+
<button danger @click=${() => this.deleteRecipe(row.name)}><md-icon>delete</md-icon>${i18next.t('figure.button.recipe-delete')}</button>`
|
|
242
|
+
: html `<button primary @click=${() => this.copyIn(row)}>${i18next.t('figure.button.recipe-copy-in')}</button>`}
|
|
243
|
+
<button @click=${() => this.edit(row)}><md-icon>data_object</md-icon>${i18next.t('figure.button.recipe-edit-source')}</button>
|
|
244
|
+
</div>
|
|
245
|
+
${this.editing
|
|
246
|
+
? html `<p muted>${i18next.t('figure.text.recipe-edit-source')}</p>
|
|
247
|
+
<textarea .value=${this.text} @input=${(e) => (this.text = e.target.value)}></textarea>
|
|
248
|
+
<div actions>
|
|
249
|
+
<button primary @click=${() => this.saveText(own ? row.state : 'draft')}>${i18next.t('figure.button.save')}</button>
|
|
250
|
+
<button @click=${() => (this.editing = false)}>${i18next.t('figure.button.close')}</button>
|
|
251
|
+
</div>`
|
|
252
|
+
: nothing}
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
/** The recipe put into the room it is checked in, with its defaults: what it makes, at several sizes. */
|
|
256
|
+
renderPreview(recipe) {
|
|
257
|
+
let asset;
|
|
258
|
+
try {
|
|
259
|
+
const room = v3RecipeTestRoom();
|
|
260
|
+
asset = applyV3Recipe(room.asset, parseV3Recipe(recipe), recipe.target === 'part' ? { target: room.target } : {}).asset;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
return html `<p role="alert">${error instanceof Error ? error.message : String(error)}</p>`;
|
|
264
|
+
}
|
|
265
|
+
return html `<p muted>${i18next.t('figure.text.recipe-preview')}</p>
|
|
266
|
+
<figure-v3-preview .asset=${asset} .context=${`recipe:${recipe.name}`}></figure-v3-preview>`;
|
|
267
|
+
}
|
|
268
|
+
choose(name) {
|
|
269
|
+
this.chosen = name;
|
|
270
|
+
this.editing = false;
|
|
271
|
+
this.error = '';
|
|
272
|
+
}
|
|
273
|
+
edit(row) {
|
|
274
|
+
this.editing = !this.editing;
|
|
275
|
+
try {
|
|
276
|
+
this.text = JSON.stringify(JSON.parse(row.source), null, 2);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
this.text = row.source;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async run(action) {
|
|
283
|
+
try {
|
|
284
|
+
await action();
|
|
285
|
+
this.error = '';
|
|
286
|
+
await this.read();
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
setState(name, state) {
|
|
293
|
+
return this.run(() => setFigureRecipeState(name, state));
|
|
294
|
+
}
|
|
295
|
+
/* The app's own prompt, as every operato screen asks -- not the browser's confirm box (user, 2026-09-24). */
|
|
296
|
+
async deleteRecipe(name) {
|
|
297
|
+
const sure = await OxPrompt.open({
|
|
298
|
+
type: 'warning',
|
|
299
|
+
title: i18next.t('figure.button.recipe-delete'),
|
|
300
|
+
text: i18next.t('figure.text.recipe-delete-confirm', { name }),
|
|
301
|
+
confirmButton: { text: i18next.t('figure.button.recipe-delete') },
|
|
302
|
+
cancelButton: { text: i18next.t('figure.button.close') }
|
|
303
|
+
});
|
|
304
|
+
if (sure)
|
|
305
|
+
await this.run(() => deleteFigureRecipe(name));
|
|
306
|
+
}
|
|
307
|
+
/** A parent's recipe copied into this domain as a draft, to be changed here. */
|
|
308
|
+
copyIn(row) {
|
|
309
|
+
return this.run(() => saveFigureRecipe(row.source, 'draft'));
|
|
310
|
+
}
|
|
311
|
+
/** The source as typed: checked by the server in the test room; a new name is a new recipe. */
|
|
312
|
+
async saveText(state) {
|
|
313
|
+
let name = '';
|
|
314
|
+
try {
|
|
315
|
+
name = JSON.parse(this.text).name ?? '';
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
this.error = error instanceof Error ? error.message : String(error);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await this.run(async () => {
|
|
322
|
+
await saveFigureRecipe(this.text, state);
|
|
323
|
+
this.chosen = name;
|
|
324
|
+
this.editing = false;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
__decorate([
|
|
329
|
+
state(),
|
|
330
|
+
__metadata("design:type", Array)
|
|
331
|
+
], FigureRecipePage.prototype, "rows", void 0);
|
|
332
|
+
__decorate([
|
|
333
|
+
state(),
|
|
334
|
+
__metadata("design:type", Object)
|
|
335
|
+
], FigureRecipePage.prototype, "chosen", void 0);
|
|
336
|
+
__decorate([
|
|
337
|
+
state(),
|
|
338
|
+
__metadata("design:type", Object)
|
|
339
|
+
], FigureRecipePage.prototype, "error", void 0);
|
|
340
|
+
__decorate([
|
|
341
|
+
state(),
|
|
342
|
+
__metadata("design:type", Object)
|
|
343
|
+
], FigureRecipePage.prototype, "editing", void 0);
|
|
344
|
+
__decorate([
|
|
345
|
+
state(),
|
|
346
|
+
__metadata("design:type", Object)
|
|
347
|
+
], FigureRecipePage.prototype, "text", void 0);
|
|
348
|
+
FigureRecipePage = __decorate([
|
|
349
|
+
customElement('figure-recipe-page')
|
|
350
|
+
], FigureRecipePage);
|
|
351
|
+
export { FigureRecipePage };
|
|
352
|
+
//# sourceMappingURL=figure-recipe-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"figure-recipe-page.js","sourceRoot":"","sources":["../../client/pages/figure-recipe-page.ts"],"names":[],"mappings":"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","sourcesContent":["import '@material/web/icon/icon.js'\nimport '../modeller/figure-v3-preview.js'\n\nimport { css, html, nothing } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { PageView } from '@operato/shell'\nimport { OxPrompt } from '@operato/popup'\nimport { ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'\nimport { applyV3Recipe, parseV3Recipe, v3RecipeTestRoom } from '@hatiolab/figure-model'\nimport type { V3Asset, V3Recipe } from '@hatiolab/figure-model'\n\nimport { deleteFigureRecipe, fetchFigureRecipeRows, saveFigureRecipe, setFigureRecipeState, type FigureRecipeRow } from '../graphql/index.js'\nimport { paramRangeOf, recipeFieldOf, recipeIsOwn, recipesOf, recipeWord, subdomainOf } from '../modeller/v3-recipes.js'\n\n/**\n * The recipes (ADR-0095): what each one puts in, shown put into a room at several sizes, and whose it is.\n *\n * A recipe of this domain's own can be released, taken back to draft, changed or deleted. A parent's -- the ones the\n * system domain ships -- is copied in under its name to be changed, and the copy stands in for it. Figures a recipe was\n * put into keep their parts whatever happens here.\n */\n@customElement('figure-recipe-page')\nexport class FigureRecipePage extends localize(i18next)(PageView) {\n static styles = [\n WorkSurfacePageStyles,\n ScrollbarStyles,\n css`\n :host {\n display: grid;\n grid-template-columns: minmax(220px, 280px) 1fr;\n background-color: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n font-size: 0.85rem;\n }\n @container (max-width: 640px) {\n :host {\n grid-template-columns: 1fr;\n }\n }\n nav {\n overflow: auto;\n border-right: 1px solid var(--md-sys-color-outline-variant);\n padding: 8px;\n }\n nav p[intro] {\n margin: 4px 4px 10px;\n color: var(--md-sys-color-on-surface-variant);\n line-height: 1.5;\n }\n button[row] {\n display: grid;\n gap: 2px;\n width: 100%;\n padding: 8px 10px;\n text-align: left;\n font: inherit;\n color: inherit;\n background: none;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n }\n button[row][on] {\n background: var(--md-sys-color-secondary-container);\n color: var(--md-sys-color-on-secondary-container);\n }\n button[row] small {\n display: flex;\n gap: 6px;\n color: var(--md-sys-color-on-surface-variant);\n }\n span[chip] {\n padding: 0 6px;\n border-radius: 999em;\n border: 1px solid var(--md-sys-color-outline-variant);\n font-size: 0.7rem;\n }\n span[chip][draft] {\n color: var(--md-sys-color-tertiary);\n border-color: var(--md-sys-color-tertiary);\n }\n main {\n overflow: auto;\n padding: 12px 16px;\n display: grid;\n gap: 12px;\n align-content: start;\n }\n main h2 {\n margin: 0;\n font-size: var(--ui-t-title, 16px);\n }\n main p {\n margin: 0;\n line-height: 1.5;\n }\n p[muted] {\n color: var(--md-sys-color-on-surface-variant);\n }\n table {\n border-collapse: collapse;\n width: 100%;\n max-width: 720px;\n }\n th,\n td {\n text-align: left;\n padding: 4px 8px;\n border-bottom: 1px solid var(--md-sys-color-outline-variant);\n }\n figure-v3-preview {\n height: 420px;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 8px;\n overflow: hidden;\n }\n div[actions] {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n }\n div[actions] button {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 6px 12px;\n font: inherit;\n color: var(--md-sys-color-on-surface);\n background: none;\n border: 1px solid var(--md-sys-color-outline-variant);\n border-radius: 999em;\n cursor: pointer;\n }\n div[actions] button[primary] {\n color: var(--md-sys-color-on-primary);\n background: var(--md-sys-color-primary);\n border-color: transparent;\n }\n div[actions] button[danger] {\n color: var(--md-sys-color-error);\n border-color: var(--md-sys-color-error);\n }\n p[role='alert'] {\n padding: 8px;\n white-space: pre-wrap;\n color: var(--md-sys-color-on-error-container);\n background: var(--md-sys-color-error-container);\n border-radius: 8px;\n }\n textarea {\n width: 100%;\n min-height: 320px;\n font: 0.78rem/1.4 monospace;\n box-sizing: border-box;\n }\n `\n ]\n\n @state() private rows: FigureRecipeRow[] = []\n @state() private chosen = ''\n @state() private error = ''\n @state() private editing = false\n @state() private text = ''\n\n get context() {\n return { title: i18next.t('figure.title.recipes') }\n }\n\n async pageUpdated(_changes: Record<string, unknown>, _lifecycle: unknown, before: { active?: boolean }) {\n if (this.active && !before?.active) await this.read()\n }\n\n private async read() {\n try {\n this.rows = await fetchFigureRecipeRows()\n if (!this.rows.some(r => r.name === this.chosen)) this.chosen = this.rows[0]?.name ?? ''\n this.error = ''\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n }\n }\n\n private get subdomain() {\n return subdomainOf(location.pathname)\n }\n\n render() {\n const language = i18next.language || 'en'\n const recipes = recipesOf(this.rows)\n const row = this.rows.find(r => r.name === this.chosen)\n const recipe = recipes.find(r => r.name === this.chosen)\n return html`\n <nav>\n <p intro>${i18next.t('figure.text.recipes-intro')}</p>\n ${this.rows.map(one => {\n const r = recipes.find(x => x.name === one.name)\n return html`<button row ?on=${one.name === this.chosen} @click=${() => this.choose(one.name)}>\n <span>${r ? recipeWord(r.title, language) : one.name}</span>\n <small>\n <span>${one.name}</span>\n ${one.state === 'draft' ? html`<span chip draft>${i18next.t('figure.label.recipe-draft')}</span>` : nothing}\n ${recipeIsOwn(one, this.subdomain) ? nothing : html`<span chip>${i18next.t('figure.label.recipe-inherited')}</span>`}\n </small>\n </button>`\n })}\n </nav>\n <main>\n ${this.error ? html`<p role=\"alert\">${this.error}</p>` : nothing}\n ${row && recipe ? this.renderRecipe(row, recipe, language) : html`<p muted>${i18next.t('figure.text.recipes-empty')}</p>`}\n </main>\n `\n }\n\n private renderRecipe(row: FigureRecipeRow, recipe: V3Recipe, language: string) {\n const own = recipeIsOwn(row, this.subdomain)\n return html`\n <h2>${recipeWord(recipe.title, language)}</h2>\n <p>${recipeWord(recipe.description, language)}</p>\n <p muted>\n ${i18next.t(recipe.target === 'part' ? 'figure.text.recipe-target-part' : 'figure.text.recipe-target-figure')} ·\n ${i18next.t(own ? 'figure.text.recipe-own' : 'figure.text.recipe-inherited', { domain: row.domain?.subdomain ?? '' })}\n </p>\n <table>\n <thead>\n <tr>\n <th>${i18next.t('figure.label.recipe-param')}</th>\n <th>${i18next.t('figure.label.recipe-default')}</th>\n <th>${i18next.t('figure.label.recipe-range')}</th>\n </tr>\n </thead>\n <tbody>\n ${recipe.params.map(\n p => html`<tr>\n <td>${recipeWord(p.label, language) || p.name}</td>\n <td>${p.kind === 'flag' ? i18next.t(p.default ? 'figure.label.yes' : 'figure.label.no') : recipeFieldOf(p, p.default)}${p.kind === 'ratio' ? ' %' : ''}</td>\n <td>${paramRangeOf(p)}</td>\n </tr>`\n )}\n </tbody>\n </table>\n ${this.renderPreview(recipe)}\n <div actions>\n ${own\n ? html`${row.state === 'draft'\n ? html`<button primary @click=${() => this.setState(row.name, 'released')}>${i18next.t('figure.button.recipe-release')}</button>`\n : html`<button @click=${() => this.setState(row.name, 'draft')}>${i18next.t('figure.button.recipe-to-draft')}</button>`}\n <button danger @click=${() => this.deleteRecipe(row.name)}><md-icon>delete</md-icon>${i18next.t('figure.button.recipe-delete')}</button>`\n : html`<button primary @click=${() => this.copyIn(row)}>${i18next.t('figure.button.recipe-copy-in')}</button>`}\n <button @click=${() => this.edit(row)}><md-icon>data_object</md-icon>${i18next.t('figure.button.recipe-edit-source')}</button>\n </div>\n ${this.editing\n ? html`<p muted>${i18next.t('figure.text.recipe-edit-source')}</p>\n <textarea .value=${this.text} @input=${(e: Event) => (this.text = (e.target as HTMLTextAreaElement).value)}></textarea>\n <div actions>\n <button primary @click=${() => this.saveText(own ? row.state : 'draft')}>${i18next.t('figure.button.save')}</button>\n <button @click=${() => (this.editing = false)}>${i18next.t('figure.button.close')}</button>\n </div>`\n : nothing}\n `\n }\n\n /** The recipe put into the room it is checked in, with its defaults: what it makes, at several sizes. */\n private renderPreview(recipe: V3Recipe) {\n let asset: V3Asset\n try {\n const room = v3RecipeTestRoom()\n asset = applyV3Recipe(room.asset, parseV3Recipe(recipe), recipe.target === 'part' ? { target: room.target } : {}).asset\n } catch (error) {\n return html`<p role=\"alert\">${error instanceof Error ? error.message : String(error)}</p>`\n }\n return html`<p muted>${i18next.t('figure.text.recipe-preview')}</p>\n <figure-v3-preview .asset=${asset} .context=${`recipe:${recipe.name}`}></figure-v3-preview>`\n }\n\n private choose(name: string) {\n this.chosen = name\n this.editing = false\n this.error = ''\n }\n\n private edit(row: FigureRecipeRow) {\n this.editing = !this.editing\n try {\n this.text = JSON.stringify(JSON.parse(row.source), null, 2)\n } catch {\n this.text = row.source\n }\n }\n\n private async run(action: () => Promise<unknown>) {\n try {\n await action()\n this.error = ''\n await this.read()\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n }\n }\n\n private setState(name: string, state: 'draft' | 'released') {\n return this.run(() => setFigureRecipeState(name, state))\n }\n\n /* The app's own prompt, as every operato screen asks -- not the browser's confirm box (user, 2026-09-24). */\n private async deleteRecipe(name: string) {\n const sure = await OxPrompt.open({\n type: 'warning',\n title: i18next.t('figure.button.recipe-delete'),\n text: i18next.t('figure.text.recipe-delete-confirm', { name }),\n confirmButton: { text: i18next.t('figure.button.recipe-delete') },\n cancelButton: { text: i18next.t('figure.button.close') }\n })\n if (sure) await this.run(() => deleteFigureRecipe(name))\n }\n\n /** A parent's recipe copied into this domain as a draft, to be changed here. */\n private copyIn(row: FigureRecipeRow) {\n return this.run(() => saveFigureRecipe(row.source, 'draft'))\n }\n\n /** The source as typed: checked by the server in the test room; a new name is a new recipe. */\n private async saveText(state: 'draft' | 'released') {\n let name = ''\n try {\n name = JSON.parse(this.text).name ?? ''\n } catch (error) {\n this.error = error instanceof Error ? error.message : String(error)\n return\n }\n await this.run(async () => {\n await saveFigureRecipe(this.text, state)\n this.chosen = name\n this.editing = false\n })\n }\n}\n"]}
|
|
@@ -17,6 +17,9 @@ export declare class FigureV3ModellerPage extends PageView {
|
|
|
17
17
|
private routeId;
|
|
18
18
|
private discardApproved;
|
|
19
19
|
private revision;
|
|
20
|
+
/** The recipes this domain sees (ADR-0095), read once when the page is first shown. */
|
|
21
|
+
private recipes;
|
|
22
|
+
private recipesRead;
|
|
20
23
|
private error;
|
|
21
24
|
/**
|
|
22
25
|
* 발행 패널이 읽는 행의 사실 — 상태와 발행 판 번호. 원본은 세션이 들고, 이 둘은 행에 있다.
|
|
@@ -77,6 +80,7 @@ export declare class FigureV3ModellerPage extends PageView {
|
|
|
77
80
|
* page on a saved figure leaves a new figure's proposal untaken, and the handler opens a new figure for it.
|
|
78
81
|
*/
|
|
79
82
|
private applyAI;
|
|
83
|
+
private holdUnsaved;
|
|
80
84
|
connectedCallback(): void;
|
|
81
85
|
disconnectedCallback(): void;
|
|
82
86
|
canDeactivate(): Promise<boolean>;
|