@webda/debug 4.0.0-beta.5 → 4.0.0-beta.7

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.
@@ -1,213 +0,0 @@
1
- import { h } from "preact";
2
- import { useState } from "preact/hooks";
3
- import htm from "htm";
4
- import { ModelGraph } from "./model-graph.js";
5
-
6
- const html = htm.bind(h);
7
-
8
- export function ModelsPanel({ data }) {
9
- const [selected, setSelected] = useState("__graph");
10
- const [filter, setFilter] = useState("");
11
- const models = data || [];
12
- const detail = selected && selected !== "__graph" ? models.find((m) => m.id === selected) || null : null;
13
-
14
- const filtered = models.filter(
15
- (m) => (m.id || m.name || "").toLowerCase().includes(filter.toLowerCase())
16
- );
17
-
18
- return html`
19
- <div class="split-panel">
20
- <div class="split-left">
21
- <input
22
- class="search-input"
23
- placeholder="Filter models..."
24
- value=${filter}
25
- onInput=${(e) => setFilter(e.target.value)}
26
- />
27
- <div
28
- class="list-item ${selected === "__graph" ? "active" : ""}"
29
- onClick=${() => setSelected("__graph")}
30
- style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border);margin-bottom:0.5rem;padding-bottom:0.625rem"
31
- >
32
- <span style="font-weight:500">Model Graph</span>
33
- <span class="badge badge-muted">${models.length}</span>
34
- </div>
35
- ${filtered.map(
36
- (m) => html`
37
- <div
38
- key=${m.id || m.name}
39
- class="list-item ${selected === (m.id || m.name) ? "active" : ""}"
40
- onClick=${() => setSelected(m.id || m.name)}
41
- >
42
- ${m.id || m.name}
43
- </div>
44
- `
45
- )}
46
- ${filtered.length === 0 && html`<div style="color: var(--text-muted); padding: 0.5rem;">No models found</div>`}
47
- </div>
48
- <div class="split-right">
49
- ${selected === "__graph"
50
- ? html`<${ModelGraph} models=${models} selectedId=${null} onSelect=${id => setSelected(id)} />`
51
- : detail ? html`<${ModelDetail} model=${detail} onSelect=${id => setSelected(id)} />`
52
- : html`
53
- <div style="color: var(--text-muted); padding: 2rem; text-align: center;">
54
- Select a model to view details
55
- </div>
56
- `}
57
- </div>
58
- </div>
59
- `;
60
- }
61
-
62
- function SchemaTab({ schemas }) {
63
- const [activeTab, setActiveTab] = useState("Input");
64
- const tabs = ["Input", "Output", "Stored"].filter((t) => schemas?.[t]);
65
-
66
- if (tabs.length === 0) {
67
- return html`<div style="color: var(--text-muted); padding: 0.5rem;">No schemas available</div>`;
68
- }
69
-
70
- // Default to first available tab if active isn't available
71
- const current = tabs.includes(activeTab) ? activeTab : tabs[0];
72
-
73
- return html`
74
- <div>
75
- <div style="display: flex; gap: 0; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border);">
76
- ${tabs.map(
77
- (tab) => html`
78
- <button
79
- key=${tab}
80
- onClick=${() => setActiveTab(tab)}
81
- style="
82
- padding: 6px 16px;
83
- background: ${current === tab ? "var(--bg-tertiary)" : "transparent"};
84
- color: ${current === tab ? "var(--accent)" : "var(--text-muted)"};
85
- border: none;
86
- border-bottom: 2px solid ${current === tab ? "var(--accent)" : "transparent"};
87
- cursor: pointer;
88
- font-size: 0.875rem;
89
- font-weight: ${current === tab ? "600" : "400"};
90
- "
91
- >
92
- ${tab}
93
- </button>
94
- `
95
- )}
96
- </div>
97
- <pre class="mono" style="
98
- background: var(--bg-secondary);
99
- padding: 1rem;
100
- border-radius: 4px;
101
- overflow: auto;
102
- max-height: 400px;
103
- font-size: 0.8125rem;
104
- line-height: 1.5;
105
- ">${JSON.stringify(schemas[current], null, 2)}</pre>
106
- </div>
107
- `;
108
- }
109
-
110
- function ModelLink({ id, onSelect }) {
111
- return html`<a href="#" class="mono" style="color:var(--accent);text-decoration:none" onClick=${e => { e.preventDefault(); onSelect(id); }}>${id.split("/").pop()}</a>`;
112
- }
113
-
114
- function RelationsTable({ relations, onSelect }) {
115
- const rows = [];
116
- if (relations.parent) {
117
- rows.push({ name: relations.parent.attribute, type: "parent", target: relations.parent.model });
118
- }
119
- (relations.links || []).forEach(l => rows.push({ name: l.attribute, type: l.type || "link", target: l.model }));
120
- (relations.queries || []).forEach(q => rows.push({ name: q.attribute, type: "query", target: q.model }));
121
- (relations.maps || []).forEach(m => rows.push({ name: m.attribute, type: "map", target: m.model }));
122
- (relations.children || []).forEach(c => rows.push({ name: "", type: "child", target: c }));
123
- (relations.binaries || []).forEach(b => rows.push({ name: b.attribute, type: `binary (${b.cardinality})`, target: "" }));
124
- (relations.behaviors || []).forEach(b => rows.push({ name: b.attribute, type: "behavior", target: b.behavior }));
125
- if (rows.length === 0) return null;
126
- return html`
127
- <div class="detail-section">
128
- <h3>Relations</h3>
129
- <div class="table-container">
130
- <table>
131
- <thead><tr><th>Attribute</th><th>Type</th><th>Target</th></tr></thead>
132
- <tbody>
133
- ${rows.map((r, i) => html`
134
- <tr key=${i}>
135
- <td class="mono">${r.name || "-"}</td>
136
- <td><span class="badge ${r.type === "parent" ? "badge-purple" : r.type === "child" ? "badge-purple" : r.type === "query" ? "badge-green" : r.type.startsWith("binary") ? "badge-orange" : r.type === "behavior" ? "badge-orange" : "badge-blue"}">${r.type}</span></td>
137
- <td>${r.target ? html`<${ModelLink} id=${r.target} onSelect=${onSelect} />` : "-"}</td>
138
- </tr>
139
- `)}
140
- </tbody>
141
- </table>
142
- </div>
143
- </div>
144
- `;
145
- }
146
-
147
- function ModelDetail({ model, onSelect }) {
148
- const ancestors = model.metadata?.Ancestors || [];
149
- const subclasses = model.metadata?.Subclasses || [];
150
-
151
- return html`
152
- <div>
153
- <h2 style="margin-bottom:0.5rem;color:var(--accent)">${model.id?.split("/").pop()}</h2>
154
- <div class="mono" style="font-size:0.75rem;color:var(--text-muted);margin-bottom:1rem">${model.id}</div>
155
-
156
- <div style="display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap;align-items:center">
157
- ${model.store && html`
158
- <div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
159
- <span style="color:var(--text-muted)">Store: </span>
160
- <span class="mono">${model.store}</span>
161
- ${model.storeType && html`<span style="color:var(--text-muted)"> (${model.storeType})</span>`}
162
- </div>
163
- `}
164
- ${model.plural && html`
165
- <div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
166
- <span style="color:var(--text-muted)">Plural: </span>
167
- <span class="mono">${model.plural}</span>
168
- </div>
169
- `}
170
- </div>
171
-
172
- ${ancestors.length > 0 && html`
173
- <div class="detail-section">
174
- <h3>Inheritance</h3>
175
- <div style="display:flex;align-items:center;gap:0.375rem;flex-wrap:wrap;font-size:0.8125rem">
176
- ${[...ancestors].reverse().map(a => html`
177
- <${ModelLink} key=${a} id=${a} onSelect=${onSelect} />
178
- <span style="color:var(--text-muted)">\u2192</span>
179
- `)}
180
- <span class="mono" style="font-weight:600;color:var(--accent)">${model.id?.split("/").pop()}</span>
181
- ${subclasses.length > 0 && html`
182
- <span style="color:var(--text-muted)">\u2192</span>
183
- ${subclasses.map((s, i) => html`
184
- <${ModelLink} key=${s} id=${s} onSelect=${onSelect} />
185
- ${i < subclasses.length - 1 && html`<span style="color:var(--text-muted)">,</span>`}
186
- `)}
187
- `}
188
- </div>
189
- </div>
190
- `}
191
-
192
- <${RelationsTable} relations=${model.relations || {}} onSelect=${onSelect} />
193
-
194
- ${model.actions && model.actions.length > 0 && html`
195
- <div class="detail-section">
196
- <h3>Actions</h3>
197
- <div style="display:flex;gap:0.375rem;flex-wrap:wrap">
198
- ${model.actions.map(a => html`
199
- <span key=${typeof a === "string" ? a : a.name} class="badge badge-blue">${typeof a === "string" ? a : a.name}</span>
200
- `)}
201
- </div>
202
- </div>
203
- `}
204
-
205
- ${model.schemas && html`
206
- <div class="detail-section">
207
- <h3>Schemas</h3>
208
- <${SchemaTab} schemas=${model.schemas} />
209
- </div>
210
- `}
211
- </div>
212
- `;
213
- }
@@ -1,315 +0,0 @@
1
- import { h } from "preact";
2
- import { useState, useMemo } from "preact/hooks";
3
- import htm from "htm";
4
- import { SchemaForm } from "./schema-form.js";
5
-
6
- const html = htm.bind(h);
7
-
8
- /**
9
- * Generate a random example value from a JSON Schema property.
10
- */
11
- function randomValue(schema, name) {
12
- if (!schema) return null;
13
- if (schema.enum) return schema.enum[Math.floor(Math.random() * schema.enum.length)];
14
- if (schema.const) return schema.const;
15
- switch (schema.type) {
16
- case "string":
17
- if (schema.format === "email") return "user@example.com";
18
- if (schema.format === "uri" || schema.format === "url") return "https://example.com";
19
- if (schema.format === "date-time") return new Date().toISOString();
20
- if (schema.format === "date") return new Date().toISOString().split("T")[0];
21
- if (schema.format === "uuid") return crypto.randomUUID();
22
- if (schema.pattern) return `match-${name || "value"}`;
23
- if (schema.minLength) return (name || "text").padEnd(schema.minLength, "x");
24
- return name || "string-value";
25
- case "number":
26
- case "integer":
27
- const min = schema.minimum ?? 0;
28
- const max = schema.maximum ?? 100;
29
- return schema.type === "integer" ? Math.floor(Math.random() * (max - min) + min) : +(Math.random() * (max - min) + min).toFixed(2);
30
- case "boolean":
31
- return Math.random() > 0.5;
32
- case "array":
33
- const item = schema.items ? randomValue(schema.items, name) : "item";
34
- return [item, randomValue(schema.items, name)].filter(Boolean);
35
- case "object":
36
- if (!schema.properties) return {};
37
- const obj = {};
38
- for (const [k, v] of Object.entries(schema.properties)) {
39
- obj[k] = randomValue(v, k);
40
- }
41
- return obj;
42
- default:
43
- return null;
44
- }
45
- }
46
-
47
- export function OperationsPanel({ data }) {
48
- const [filter, setFilter] = useState("");
49
- const [selected, setSelected] = useState(null);
50
- const [activeTab, setActiveTab] = useState("form");
51
- const [formValues, setFormValues] = useState({});
52
- const operations = data || [];
53
-
54
- const filtered = operations.filter(
55
- (o) => (o.id || "").toLowerCase().includes(filter.toLowerCase())
56
- );
57
-
58
- const detail = selected ? operations.find(o => o.id === selected) : null;
59
-
60
- const exampleOutput = useMemo(() => {
61
- if (!detail?.outputSchema) return null;
62
- return randomValue(detail.outputSchema, "result");
63
- }, [detail?.id, detail?.outputSchema]);
64
-
65
- return html`
66
- <div class="split-panel">
67
- <div class="split-left">
68
- <input
69
- class="search-input"
70
- placeholder="Filter operations..."
71
- value=${filter}
72
- onInput=${(e) => setFilter(e.target.value)}
73
- />
74
- ${filtered.map(o => html`
75
- <div
76
- key=${o.id}
77
- class="list-item ${selected === o.id ? "active" : ""}"
78
- onClick=${() => { setSelected(o.id); setFormValues({}); setActiveTab("form"); }}
79
- >
80
- <div>${o.id}</div>
81
- <div style="font-size:0.6875rem;color:var(--text-muted)">
82
- ${o.input && o.input !== "void" ? o.input.split("/").pop().split(".").pop() : "void"}
83
- ${" → "}
84
- ${o.output && o.output !== "void" ? o.output.split("/").pop().split(".").pop() : "void"}
85
- </div>
86
- ${o.rest?.method && html`
87
- <div style="font-size:0.625rem;margin-top:0.125rem;display:flex;align-items:center;gap:0.25rem;min-width:0">
88
- <span class="badge method-${o.rest.method.toLowerCase()}" style="font-size:0.5625rem;padding:0.0625rem 0.25rem;flex-shrink:0">${o.rest.method.toUpperCase()}</span>
89
- <span class="mono" style="color:var(--text-muted);opacity:0.7;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${o.rest.url || o.rest.path || "/"}</span>
90
- </div>
91
- `}
92
- </div>
93
- `)}
94
- ${filtered.length === 0 && html`<div style="color:var(--text-muted);padding:0.5rem">No operations found</div>`}
95
- </div>
96
- <div class="split-right">
97
- ${detail ? html`<${OperationDetail}
98
- op=${detail}
99
- activeTab=${activeTab}
100
- setActiveTab=${setActiveTab}
101
- formValues=${formValues}
102
- setFormValues=${setFormValues}
103
- exampleOutput=${exampleOutput}
104
- />` : html`
105
- <div style="color:var(--text-muted);padding:2rem;text-align:center">
106
- Select an operation to view details
107
- </div>
108
- `}
109
- </div>
110
- </div>
111
- `;
112
- }
113
-
114
- /**
115
- * Lightweight JS syntax highlighter — no external dependencies.
116
- * Tokenizes into comments, strings, keywords, numbers, and punctuation.
117
- */
118
- function highlightJS(code) {
119
- const keywords = new Set([
120
- "async","await","break","case","catch","class","const","continue","debugger","default",
121
- "delete","do","else","export","extends","finally","for","function","if","import","in",
122
- "instanceof","let","new","of","return","static","super","switch","this","throw","try",
123
- "typeof","var","void","while","with","yield","from","true","false","null","undefined"
124
- ]);
125
- const tokens = [];
126
- let i = 0;
127
- while (i < code.length) {
128
- // Line comments
129
- if (code[i] === "/" && code[i + 1] === "/") {
130
- const end = code.indexOf("\n", i);
131
- tokens.push({ type: "comment", value: code.slice(i, end === -1 ? undefined : end) });
132
- i = end === -1 ? code.length : end;
133
- continue;
134
- }
135
- // Block comments
136
- if (code[i] === "/" && code[i + 1] === "*") {
137
- const end = code.indexOf("*/", i + 2);
138
- tokens.push({ type: "comment", value: code.slice(i, end === -1 ? undefined : end + 2) });
139
- i = end === -1 ? code.length : end + 2;
140
- continue;
141
- }
142
- // Strings
143
- if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
144
- const q = code[i];
145
- let j = i + 1;
146
- while (j < code.length && code[j] !== q) { if (code[j] === "\\") j++; j++; }
147
- tokens.push({ type: "string", value: code.slice(i, j + 1) });
148
- i = j + 1;
149
- continue;
150
- }
151
- // Numbers
152
- if (/[0-9]/.test(code[i]) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))) {
153
- let j = i;
154
- while (j < code.length && /[0-9a-fA-FxXoObBeE._n]/.test(code[j])) j++;
155
- tokens.push({ type: "number", value: code.slice(i, j) });
156
- i = j;
157
- continue;
158
- }
159
- // Words (identifiers / keywords)
160
- if (/[a-zA-Z_$]/.test(code[i])) {
161
- let j = i;
162
- while (j < code.length && /[a-zA-Z0-9_$]/.test(code[j])) j++;
163
- const word = code.slice(i, j);
164
- tokens.push({ type: keywords.has(word) ? "keyword" : "ident", value: word });
165
- i = j;
166
- continue;
167
- }
168
- // Punctuation / operators
169
- tokens.push({ type: "punct", value: code[i] });
170
- i++;
171
- }
172
- return tokens;
173
- }
174
-
175
- function CodeBlock({ code }) {
176
- const tokens = useMemo(() => highlightJS(code), [code]);
177
- return html`<pre class="code-block"><code>${tokens.map((t, i) =>
178
- t.type === "comment" ? html`<span key=${i} class="hl-comment">${t.value}</span>`
179
- : t.type === "string" ? html`<span key=${i} class="hl-string">${t.value}</span>`
180
- : t.type === "keyword" ? html`<span key=${i} class="hl-keyword">${t.value}</span>`
181
- : t.type === "number" ? html`<span key=${i} class="hl-number">${t.value}</span>`
182
- : t.value
183
- )}</code></pre>`;
184
- }
185
-
186
- function OperationDetail({ op, activeTab, setActiveTab, formValues, setFormValues, exampleOutput }) {
187
- const tabs = [];
188
- if (op.inputSchema) tabs.push("form");
189
- tabs.push("input-schema", "output-schema");
190
- if (exampleOutput !== null) tabs.push("example");
191
- if (op.implementor?.code) tabs.push("code");
192
-
193
- const currentTab = tabs.includes(activeTab) ? activeTab : tabs[0];
194
-
195
- const hasInput = op.input && op.input !== "void";
196
- const hasOutput = op.output && op.output !== "void";
197
-
198
- return html`
199
- <div>
200
- <h2 style="margin-bottom:0.5rem;color:var(--accent)">${op.id}</h2>
201
-
202
- <div style="display:flex;gap:0.75rem;margin-bottom:1rem;flex-wrap:wrap">
203
- ${op.summary && html`<div style="color:var(--text-muted);font-size:0.875rem">${op.summary}</div>`}
204
- </div>
205
-
206
- <div style="display:flex;gap:0.5rem;margin-bottom:0.75rem;flex-wrap:wrap;align-items:center">
207
- <div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
208
- <span style="color:var(--text-muted)">Input: </span>
209
- <span class="mono">${hasInput ? op.input : "void"}</span>
210
- </div>
211
- <div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
212
- <span style="color:var(--text-muted)">Output: </span>
213
- <span class="mono">${hasOutput ? op.output : "void"}</span>
214
- </div>
215
- ${op.implementor && html`
216
- <div style="background:var(--bg-tertiary);padding:0.375rem 0.625rem;border-radius:4px;font-size:0.8125rem">
217
- <span style="color:var(--text-muted)">${op.implementor.type === "model" ? "Model" : "Service"}: </span>
218
- <span class="mono">${op.implementor.name}</span>
219
- ${op.implementor.method && html`<span class="mono" style="color:var(--text-muted)">.${op.implementor.method}()</span>`}
220
- </div>
221
- `}
222
- ${op.tags?.length > 0 && op.tags.map(t => html`
223
- <span key=${t} class="badge badge-purple">${t}</span>
224
- `)}
225
- ${op.rest && typeof op.rest === "object" && op.rest.method && html`
226
- <span class="badge method-${op.rest.method.toLowerCase()}" style="padding:0.125rem 0.5rem;line-height:1.2">${op.rest.method.toUpperCase()}</span>
227
- <span class="mono" style="font-size:0.8125rem;color:var(--text-muted)">${op.rest.url || op.rest.path || "/"}</span>
228
- `}
229
- </div>
230
-
231
- <!-- Tabs -->
232
- <div style="display:flex;gap:0;margin-bottom:0.75rem;border-bottom:1px solid var(--border)">
233
- ${tabs.map(tab => {
234
- const label = tab === "form" ? "Try It" : tab === "input-schema" ? "Input Schema" : tab === "output-schema" ? "Output Schema" : tab === "code" ? "Code" : "Example Output";
235
- return html`
236
- <button key=${tab} onClick=${() => setActiveTab(tab)} style="
237
- padding:6px 16px;
238
- background:${currentTab === tab ? "var(--bg-tertiary)" : "transparent"};
239
- color:${currentTab === tab ? "var(--accent)" : "var(--text-muted)"};
240
- border:none;
241
- border-bottom:2px solid ${currentTab === tab ? "var(--accent)" : "transparent"};
242
- cursor:pointer;font-size:0.875rem;
243
- font-weight:${currentTab === tab ? "600" : "400"};
244
- ">${label}</button>
245
- `;
246
- })}
247
- </div>
248
-
249
- <!-- Tab Content -->
250
- ${currentTab === "form" && op.inputSchema && html`
251
- <div>
252
- <${SchemaForm} schema=${op.inputSchema} values=${formValues} onChange=${setFormValues} />
253
- <div style="margin-top:1rem">
254
- <button class="btn btn-primary" disabled>Execute (coming soon)</button>
255
- </div>
256
- ${Object.keys(formValues).length > 0 && html`
257
- <div style="margin-top:0.75rem">
258
- <strong style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase">Request Body</strong>
259
- <pre class="mono" style="
260
- background:var(--bg-secondary);padding:0.75rem;border-radius:4px;
261
- margin-top:0.25rem;font-size:0.8125rem;overflow:auto;max-height:200px;
262
- ">${JSON.stringify(formValues, null, 2)}</pre>
263
- </div>
264
- `}
265
- </div>
266
- `}
267
-
268
- ${currentTab === "input-schema" && html`
269
- <div>
270
- ${hasInput && op.inputSchema
271
- ? html`<pre class="mono" style="
272
- background:var(--bg-secondary);padding:1rem;border-radius:4px;
273
- overflow:auto;max-height:400px;font-size:0.8125rem;line-height:1.5;
274
- ">${JSON.stringify(op.inputSchema, null, 2)}</pre>`
275
- : html`<div style="color:var(--text-muted);padding:0.5rem">${hasInput ? `Schema ref: ${op.input} (not resolved)` : "No input (void)"}</div>`
276
- }
277
- </div>
278
- `}
279
-
280
- ${currentTab === "output-schema" && html`
281
- <div>
282
- ${hasOutput && op.outputSchema
283
- ? html`<pre class="mono" style="
284
- background:var(--bg-secondary);padding:1rem;border-radius:4px;
285
- overflow:auto;max-height:400px;font-size:0.8125rem;line-height:1.5;
286
- ">${JSON.stringify(op.outputSchema, null, 2)}</pre>`
287
- : html`<div style="color:var(--text-muted);padding:0.5rem">${hasOutput ? `Schema ref: ${op.output} (not resolved)` : "No output (void)"}</div>`
288
- }
289
- </div>
290
- `}
291
-
292
- ${currentTab === "example" && html`
293
- <div>
294
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
295
- <strong style="color:var(--text-muted);font-size:0.75rem;text-transform:uppercase">Random Example</strong>
296
- <button class="btn btn-ghost btn-sm" onClick=${() => { /* force re-render */ setActiveTab("input-schema"); setTimeout(() => setActiveTab("example"), 0); }}>Regenerate</button>
297
- </div>
298
- <pre class="mono" style="
299
- background:var(--bg-secondary);padding:1rem;border-radius:4px;
300
- overflow:auto;max-height:400px;font-size:0.8125rem;line-height:1.5;
301
- ">${JSON.stringify(exampleOutput, null, 2)}</pre>
302
- </div>
303
- `}
304
-
305
- ${currentTab === "code" && op.implementor?.code && html`
306
- <div>
307
- <div style="margin-bottom:0.5rem;font-size:0.8125rem;color:var(--text-muted)">
308
- <span class="mono">${op.implementor.name}.${op.implementor.method}()</span>
309
- </div>
310
- <${CodeBlock} code=${op.implementor.code} />
311
- </div>
312
- `}
313
- </div>
314
- `;
315
- }