@jxsuite/studio 0.28.4 → 0.29.0
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/dist/studio.js +6769 -5399
- package/dist/studio.js.map +86 -79
- package/package.json +5 -4
- package/src/browse/browse-modal.ts +2 -2
- package/src/browse/browse.ts +20 -19
- package/src/canvas/canvas-diff.ts +3 -3
- package/src/canvas/canvas-helpers.ts +15 -2
- package/src/canvas/canvas-live-render.ts +168 -86
- package/src/canvas/canvas-patcher.ts +656 -0
- package/src/canvas/canvas-perf.ts +68 -0
- package/src/canvas/canvas-render.ts +135 -23
- package/src/canvas/canvas-subtree-render.ts +113 -0
- package/src/canvas/canvas-utils.ts +4 -0
- package/src/editor/component-inline-edit.ts +4 -35
- package/src/editor/context-menu.ts +92 -74
- package/src/editor/convert-to-component.ts +11 -2
- package/src/editor/convert-to-repeater.ts +4 -5
- package/src/editor/inline-edit.ts +9 -9
- package/src/editor/inline-format.ts +11 -11
- package/src/editor/merge-tags.ts +120 -0
- package/src/editor/shortcuts.ts +4 -4
- package/src/files/components.ts +37 -0
- package/src/files/file-ops.ts +28 -7
- package/src/files/files.ts +25 -21
- package/src/github/github-auth.ts +14 -2
- package/src/github/github-publish.ts +12 -2
- package/src/panels/activity-bar.ts +1 -1
- package/src/panels/ai-panel.ts +67 -39
- package/src/panels/block-action-bar.ts +72 -1
- package/src/panels/canvas-dnd.ts +55 -26
- package/src/panels/data-explorer.ts +5 -3
- package/src/panels/dnd.ts +12 -15
- package/src/panels/editors.ts +5 -23
- package/src/panels/elements-panel.ts +2 -12
- package/src/panels/events-panel.ts +1 -1
- package/src/panels/git-panel.ts +6 -6
- package/src/panels/head-panel.ts +1 -1
- package/src/panels/layers-panel.ts +177 -147
- package/src/panels/preview-render.ts +15 -0
- package/src/panels/properties-panel.ts +16 -27
- package/src/panels/quick-search.ts +2 -2
- package/src/panels/right-panel.ts +2 -6
- package/src/panels/shared.ts +3 -0
- package/src/panels/signals-panel.ts +24 -22
- package/src/panels/statusbar.ts +15 -6
- package/src/panels/style-panel.ts +3 -3
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-panel.ts +4 -4
- package/src/panels/tab-bar.ts +198 -0
- package/src/panels/tab-strip.ts +2 -2
- package/src/panels/toolbar.ts +6 -75
- package/src/platforms/devserver.ts +60 -34
- package/src/reactivity.ts +2 -0
- package/src/services/cem-export.ts +23 -2
- package/src/services/code-services.ts +16 -2
- package/src/services/monaco-setup.ts +2 -1
- package/src/settings/content-types-editor.ts +16 -16
- package/src/settings/css-vars-editor.ts +2 -2
- package/src/settings/defs-editor.ts +14 -14
- package/src/settings/general-settings.ts +6 -6
- package/src/settings/head-editor.ts +2 -2
- package/src/site-context.ts +1 -1
- package/src/state.ts +66 -12
- package/src/store.ts +15 -3
- package/src/studio.ts +73 -33
- package/src/tabs/patch-ops.ts +143 -0
- package/src/tabs/tab.ts +15 -1
- package/src/tabs/transact.ts +454 -27
- package/src/types.ts +41 -0
- package/src/ui/color-selector.ts +7 -7
- package/src/ui/icons.ts +0 -30
- package/src/ui/media-picker.ts +2 -2
- package/src/ui/panel-resize.ts +6 -1
- package/src/ui/unit-selector.ts +2 -2
- package/src/ui/value-selector.ts +3 -3
- package/src/utils/canvas-media.ts +6 -6
- package/src/utils/edit-display.ts +125 -83
- package/src/utils/google-fonts.ts +1 -1
- package/src/utils/inherited-style.ts +3 -2
- package/src/utils/studio-utils.ts +1 -1
- package/src/view.ts +4 -1
package/src/panels/canvas-dnd.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { CanvasPanel } from "../types";
|
|
|
20
20
|
|
|
21
21
|
import type { JxPath } from "../state";
|
|
22
22
|
|
|
23
|
-
export type { CanvasPanel };
|
|
23
|
+
export type { CanvasPanel } from "../types";
|
|
24
24
|
|
|
25
25
|
interface DropInstruction {
|
|
26
26
|
type: "reorder-above" | "reorder-below" | "make-child";
|
|
@@ -125,34 +125,63 @@ export function registerPanelDnD(panel: CanvasPanel) {
|
|
|
125
125
|
});
|
|
126
126
|
view.canvasDndCleanups.push(monitorCleanup);
|
|
127
127
|
|
|
128
|
-
const document = activeTab.value?.doc.document;
|
|
129
128
|
for (const el of allEls) {
|
|
130
|
-
|
|
131
|
-
if (!elPath) {
|
|
129
|
+
if (!elToPath.get(el)) {
|
|
132
130
|
continue;
|
|
133
131
|
}
|
|
132
|
+
registerElementDropTarget(el);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Register one canvas element as a drop target. Path and leaf-ness are read live from elToPath and
|
|
138
|
+
* the current document at drag time, so surgical patches that remap sibling paths never leave stale
|
|
139
|
+
* closures behind.
|
|
140
|
+
*
|
|
141
|
+
* @param {Element} el
|
|
142
|
+
*/
|
|
143
|
+
function registerElementDropTarget(el: Element) {
|
|
144
|
+
const cleanup = dropTargetForElements({
|
|
145
|
+
canDrop({ source }) {
|
|
146
|
+
const elPath = elToPath.get(el);
|
|
147
|
+
if (!elPath) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const srcPath = source.data.path as JxPath | undefined;
|
|
151
|
+
if (srcPath && isAncestor(srcPath, elPath)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
},
|
|
156
|
+
element: /** @type {HTMLElement} */ el,
|
|
157
|
+
getData() {
|
|
158
|
+
const elPath = elToPath.get(el) ?? [];
|
|
159
|
+
const document = activeTab.value?.doc.document;
|
|
160
|
+
const node = document ? getNodeAtPath(document, elPath) : undefined;
|
|
161
|
+
const tag = (node?.tagName || "div").toLowerCase();
|
|
162
|
+
const hasElementChildren =
|
|
163
|
+
Array.isArray(node?.children) &&
|
|
164
|
+
node.children.some((c: unknown) => c != null && typeof c === "object");
|
|
165
|
+
return { _isVoid: VOID_ELEMENTS.has(tag) || !hasElementChildren, path: elPath };
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
view.canvasDndCleanups.push(cleanup);
|
|
169
|
+
}
|
|
141
170
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Register drop targets for a freshly patched-in subtree (root plus descendants). Cleanups join
|
|
173
|
+
* view.canvasDndCleanups and are released by the next full render like all canvas DnD handlers.
|
|
174
|
+
*
|
|
175
|
+
* @param {HTMLElement} rootEl
|
|
176
|
+
*/
|
|
177
|
+
export function registerSubtreeDnD(rootEl: HTMLElement) {
|
|
178
|
+
if (elToPath.get(rootEl)) {
|
|
179
|
+
registerElementDropTarget(rootEl);
|
|
180
|
+
}
|
|
181
|
+
for (const el of rootEl.querySelectorAll("*")) {
|
|
182
|
+
if (elToPath.get(el)) {
|
|
183
|
+
registerElementDropTarget(el);
|
|
184
|
+
}
|
|
156
185
|
}
|
|
157
186
|
}
|
|
158
187
|
|
|
@@ -229,7 +258,7 @@ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath:
|
|
|
229
258
|
let closestIdx = children.length - 1;
|
|
230
259
|
|
|
231
260
|
for (let i = 0; i < children.length; i++) {
|
|
232
|
-
const rect = children[i]
|
|
261
|
+
const rect = children[i]!.getBoundingClientRect();
|
|
233
262
|
const topDist = Math.abs(cursorY - rect.top);
|
|
234
263
|
const bottomDist = Math.abs(cursorY - rect.bottom);
|
|
235
264
|
|
|
@@ -248,7 +277,7 @@ function nearestChildEdge(children: HTMLElement[], cursorY: number, parentPath:
|
|
|
248
277
|
const childPath = [...parentPath, "children", closestIdx];
|
|
249
278
|
return {
|
|
250
279
|
instruction,
|
|
251
|
-
referenceEl: children[closestIdx]
|
|
280
|
+
referenceEl: children[closestIdx]!,
|
|
252
281
|
targetPath: childPath,
|
|
253
282
|
};
|
|
254
283
|
}
|
|
@@ -9,7 +9,7 @@ import type { TemplateResult } from "lit-html";
|
|
|
9
9
|
const expandedDataKeys = new Set();
|
|
10
10
|
|
|
11
11
|
/** Unwrap a Vue ref (has .value and .__v_isRef) to get the underlying value. */
|
|
12
|
-
function unwrapSignal(value: unknown) {
|
|
12
|
+
export function unwrapSignal(value: unknown) {
|
|
13
13
|
if (value && typeof value === "object" && (value as Record<string, unknown>).__v_isRef) {
|
|
14
14
|
return (value as Record<string, unknown>).value;
|
|
15
15
|
}
|
|
@@ -17,7 +17,7 @@ function unwrapSignal(value: unknown) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Type label for a signal value in the data explorer. */
|
|
20
|
-
function dataTypeLabel(value: unknown) {
|
|
20
|
+
export function dataTypeLabel(value: unknown) {
|
|
21
21
|
const v = unwrapSignal(value);
|
|
22
22
|
if (v === null) {
|
|
23
23
|
return "null";
|
|
@@ -164,7 +164,9 @@ export function renderDataTreeTemplate(
|
|
|
164
164
|
><span class="data-value data-${item === null ? "null" : typeof item}">${valText}</span>
|
|
165
165
|
</div>`;
|
|
166
166
|
}
|
|
167
|
-
const label = Array.isArray(item)
|
|
167
|
+
const label = Array.isArray(item)
|
|
168
|
+
? `Array(${item.length})`
|
|
169
|
+
: `{${Object.keys(item as object).length}}`;
|
|
168
170
|
return html`
|
|
169
171
|
<div class="data-branch" style="padding-left:${indent}">
|
|
170
172
|
<span class="data-key">[${i}] </span
|
package/src/panels/dnd.ts
CHANGED
|
@@ -27,7 +27,11 @@ import {
|
|
|
27
27
|
import { mutateInsertNode, mutateMoveNode, transact, transactDoc } from "../tabs/transact";
|
|
28
28
|
import { activeTab } from "../workspace/workspace";
|
|
29
29
|
import { view } from "../view";
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
buildComponentInstance,
|
|
32
|
+
componentRegistry,
|
|
33
|
+
computeRelativePath,
|
|
34
|
+
} from "../files/components";
|
|
31
35
|
import { renderComponentPreview } from "./stylebook-panel";
|
|
32
36
|
import { defaultDef, unsafeTags } from "./shared";
|
|
33
37
|
import type { JxPath } from "../state";
|
|
@@ -205,25 +209,13 @@ export function registerComponentsDnD() {
|
|
|
205
209
|
// Fill preview with live rendered component
|
|
206
210
|
const preview = row.querySelector(".element-card-preview");
|
|
207
211
|
if (preview && !preview.querySelector(tagName)) {
|
|
208
|
-
renderComponentPreview(comp).then((el: HTMLElement) => {
|
|
212
|
+
void renderComponentPreview(comp).then((el: HTMLElement) => {
|
|
209
213
|
preview.textContent = "";
|
|
210
214
|
preview.append(el);
|
|
211
215
|
});
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
const instanceDef =
|
|
215
|
-
$props: comp.props
|
|
216
|
-
? Object.fromEntries(
|
|
217
|
-
comp.props.map(
|
|
218
|
-
(/** @type {{ name: string; default?: unknown; [k: string]: unknown }} */ p) => [
|
|
219
|
-
p.name,
|
|
220
|
-
p.default !== undefined ? p.default : "",
|
|
221
|
-
],
|
|
222
|
-
),
|
|
223
|
-
)
|
|
224
|
-
: {},
|
|
225
|
-
tagName: comp.tagName,
|
|
226
|
-
};
|
|
218
|
+
const instanceDef = buildComponentInstance(comp);
|
|
227
219
|
const cleanup = draggable({
|
|
228
220
|
element: row,
|
|
229
221
|
getInitialData() {
|
|
@@ -336,6 +328,11 @@ export function clearLayerDropGap(container: HTMLElement) {
|
|
|
336
328
|
const rows = container.querySelectorAll(".layers-tree .layer-row");
|
|
337
329
|
for (const r of rows) {
|
|
338
330
|
(r as HTMLElement).style.transform = "";
|
|
331
|
+
// Also clear `display:none` left by hideDescendantRows. The `.layer-row` div has no `style`
|
|
332
|
+
// Lit binding and rows aren't keyed, so lit reuses these DOM nodes positionally on the
|
|
333
|
+
// Post-drop re-render — a stale `display:none` would otherwise hide whichever row lands on the
|
|
334
|
+
// Reused node (e.g. a sibling of the moved subtree).
|
|
335
|
+
(r as HTMLElement).style.display = "";
|
|
339
336
|
}
|
|
340
337
|
}
|
|
341
338
|
|
package/src/panels/editors.ts
CHANGED
|
@@ -38,8 +38,7 @@ function getFunctionBody(editing: EditingTarget | null | undefined) {
|
|
|
38
38
|
return "";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
export function renderFunctionEditor(closeFunctionEditor: () => void) {
|
|
41
|
+
export function renderFunctionEditor() {
|
|
43
42
|
const editing = activeTab.value?.session.ui.editingFunction as EditingTarget | null | undefined;
|
|
44
43
|
|
|
45
44
|
// If editor already exists and matches current target, just sync value
|
|
@@ -79,27 +78,10 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
|
|
|
79
78
|
canvasWrap.style.flexDirection = "column";
|
|
80
79
|
canvasWrap.style.alignItems = "stretch";
|
|
81
80
|
|
|
82
|
-
//
|
|
83
|
-
renderOnly("toolbar");
|
|
84
|
-
|
|
85
|
-
const tab = activeTab.value;
|
|
86
|
-
const docName = tab?.documentPath?.split("/").pop() || tab?.doc.document?.tagName || "document";
|
|
87
|
-
const ed = editing as EditingTarget;
|
|
88
|
-
const funcLabel = ed.type === "def" ? `ƒ ${ed.defName}` : `ƒ ${ed.eventKey}`;
|
|
89
|
-
|
|
90
|
-
// Editor container
|
|
81
|
+
// The tab bar renders the Back button + breadcrumb context for the function editor.
|
|
91
82
|
let editorContainer: HTMLDivElement | null = null;
|
|
92
83
|
litRender(
|
|
93
84
|
html`<div class="source-wrap">
|
|
94
|
-
<div class="source-toolbar">
|
|
95
|
-
<sp-action-button size="s" @click=${closeFunctionEditor}>
|
|
96
|
-
<sp-icon-back slot="icon"></sp-icon-back>
|
|
97
|
-
Back
|
|
98
|
-
</sp-action-button>
|
|
99
|
-
<span class="breadcrumb-item">${docName}</span>
|
|
100
|
-
<span class="breadcrumb-sep"> › </span>
|
|
101
|
-
<span class="breadcrumb-item current">${funcLabel}</span>
|
|
102
|
-
</div>
|
|
103
85
|
<div
|
|
104
86
|
class="source-editor"
|
|
105
87
|
${ref((el) => {
|
|
@@ -135,13 +117,13 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
|
|
|
135
117
|
const editor = view.functionEditor;
|
|
136
118
|
|
|
137
119
|
// Format on open — show pretty-printed code, then run initial lint
|
|
138
|
-
codeService("format", { args, code: body }).then((result) => {
|
|
120
|
+
void codeService("format", { args, code: body }).then((result) => {
|
|
139
121
|
if (result?.code != null && view.functionEditor) {
|
|
140
122
|
view.functionEditor._ignoreNextChange = true;
|
|
141
123
|
view.functionEditor.setValue(result.code);
|
|
142
124
|
}
|
|
143
125
|
});
|
|
144
|
-
codeService("lint", { args, code: body }).then((result) => {
|
|
126
|
+
void codeService("lint", { args, code: body }).then((result) => {
|
|
145
127
|
if (result?.diagnostics && view.functionEditor) {
|
|
146
128
|
setLintMarkers(view.functionEditor, result.diagnostics as OxLintDiagnostic[]);
|
|
147
129
|
}
|
|
@@ -183,7 +165,7 @@ export function renderFunctionEditor(closeFunctionEditor: () => void) {
|
|
|
183
165
|
lintDebounce = setTimeout(() => {
|
|
184
166
|
const gen = (lintGen += 1);
|
|
185
167
|
const currentCode = editor.getValue();
|
|
186
|
-
codeService("lint", { args, code: currentCode }).then((result) => {
|
|
168
|
+
void codeService("lint", { args, code: currentCode }).then((result) => {
|
|
187
169
|
if (gen !== lintGen) {
|
|
188
170
|
return;
|
|
189
171
|
}
|
|
@@ -7,7 +7,7 @@ import { activeTab } from "../workspace/workspace";
|
|
|
7
7
|
import { mutateInsertNode, transactDoc } from "../tabs/transact";
|
|
8
8
|
import { view } from "../view";
|
|
9
9
|
import { getEffectiveElements } from "../site-context";
|
|
10
|
-
import { componentRegistry } from "../files/components";
|
|
10
|
+
import { buildComponentInstance, componentRegistry } from "../files/components";
|
|
11
11
|
|
|
12
12
|
import type { ComponentEntry } from "../files/components";
|
|
13
13
|
import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
|
|
@@ -134,17 +134,7 @@ export function renderElementsTemplate(ctx: {
|
|
|
134
134
|
const parentPath = t?.session.selection || [];
|
|
135
135
|
const parent = getNodeAtPath(t!.doc.document, parentPath);
|
|
136
136
|
const idx = childList(parent).length;
|
|
137
|
-
const instanceDef =
|
|
138
|
-
$props: Object.fromEntries(
|
|
139
|
-
(comp.props || []).map(
|
|
140
|
-
(/** @type {{ name: string; default?: unknown }} */ p) => [
|
|
141
|
-
p.name,
|
|
142
|
-
p.default !== undefined ? p.default : "",
|
|
143
|
-
],
|
|
144
|
-
),
|
|
145
|
-
),
|
|
146
|
-
tagName: comp.tagName,
|
|
147
|
-
};
|
|
137
|
+
const instanceDef = buildComponentInstance(comp);
|
|
148
138
|
transactDoc(t!, (tr) =>
|
|
149
139
|
mutateInsertNode(tr, parentPath, idx, structuredClone(instanceDef)),
|
|
150
140
|
);
|
|
@@ -266,7 +266,7 @@ export function eventsSidebarTemplate(helpers: { isCustomElementDoc: () => boole
|
|
|
266
266
|
if (functionDefs.length > 0) {
|
|
267
267
|
transactDoc(activeTab.value, (t) =>
|
|
268
268
|
mutateUpdateProperty(t, selection, evName, {
|
|
269
|
-
$ref: `#/state/${functionDefs[0][0]}`,
|
|
269
|
+
$ref: `#/state/${functionDefs[0]![0]}`,
|
|
270
270
|
}),
|
|
271
271
|
);
|
|
272
272
|
} else {
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -132,7 +132,7 @@ async function gitAction(action: string, body?: unknown) {
|
|
|
132
132
|
updateUi("gitLoading", true);
|
|
133
133
|
updateUi("gitError", null);
|
|
134
134
|
try {
|
|
135
|
-
await plat[action](body);
|
|
135
|
+
await plat[action]!(body);
|
|
136
136
|
await refreshGitStatus();
|
|
137
137
|
} catch (error) {
|
|
138
138
|
updateUi("gitError", errorMessage(error));
|
|
@@ -191,7 +191,7 @@ export function renderGitPanel(
|
|
|
191
191
|
const loading = S.ui.gitLoading;
|
|
192
192
|
|
|
193
193
|
if (!status && !loading) {
|
|
194
|
-
refreshGitStatus();
|
|
194
|
+
void refreshGitStatus();
|
|
195
195
|
return html`<div class="git-panel">
|
|
196
196
|
<div class="git-loading">Loading...</div>
|
|
197
197
|
</div>`;
|
|
@@ -232,7 +232,7 @@ export function renderGitPanel(
|
|
|
232
232
|
if (!_pollTimer) {
|
|
233
233
|
_pollTimer = setInterval(() => {
|
|
234
234
|
if (view.leftTab === "git" && !S.ui.gitLoading) {
|
|
235
|
-
refreshGitStatus();
|
|
235
|
+
void refreshGitStatus();
|
|
236
236
|
}
|
|
237
237
|
}, 30_000);
|
|
238
238
|
}
|
|
@@ -417,7 +417,7 @@ export function renderGitPanel(
|
|
|
417
417
|
const switchTab = (tab: string) => {
|
|
418
418
|
_gitSubTab = tab;
|
|
419
419
|
if (tab === "history" && !S.ui.gitLogEntries) {
|
|
420
|
-
fetchGitLog();
|
|
420
|
+
void fetchGitLog();
|
|
421
421
|
}
|
|
422
422
|
renderOnly("leftPanel");
|
|
423
423
|
};
|
|
@@ -453,7 +453,7 @@ export function renderGitPanel(
|
|
|
453
453
|
@keydown=${(e: KeyboardEvent) => {
|
|
454
454
|
if (e.ctrlKey && e.key === "Enter") {
|
|
455
455
|
e.preventDefault();
|
|
456
|
-
doCommit();
|
|
456
|
+
void doCommit();
|
|
457
457
|
}
|
|
458
458
|
}}
|
|
459
459
|
></sp-textfield>
|
|
@@ -489,7 +489,7 @@ export function renderGitPanel(
|
|
|
489
489
|
"hidden",
|
|
490
490
|
"",
|
|
491
491
|
);
|
|
492
|
-
doCommit();
|
|
492
|
+
void doCommit();
|
|
493
493
|
}}
|
|
494
494
|
>
|
|
495
495
|
Commit (don't sync)
|
package/src/panels/head-panel.ts
CHANGED
|
@@ -334,7 +334,7 @@ export function renderHeadTemplate({
|
|
|
334
334
|
let layoutSection: TemplateResult | symbol = nothing;
|
|
335
335
|
if (isPage) {
|
|
336
336
|
if (layoutEntries === null) {
|
|
337
|
-
loadLayoutEntries();
|
|
337
|
+
void loadLayoutEntries();
|
|
338
338
|
} else {
|
|
339
339
|
const currentLayout = doc.$layout;
|
|
340
340
|
const defaultLayout = projectState?.projectConfig?.defaults?.layout;
|