@jxsuite/studio 1.0.0 → 1.1.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.
Files changed (86) hide show
  1. package/dist/iframe-entry.js +622 -49
  2. package/dist/iframe-entry.js.map +15 -12
  3. package/dist/studio.css +1333 -0
  4. package/dist/studio.js +53069 -31746
  5. package/dist/studio.js.map +109 -74
  6. package/package.json +11 -9
  7. package/src/canvas/canvas-live-render.ts +21 -2
  8. package/src/canvas/canvas-render.ts +35 -16
  9. package/src/canvas/canvas-utils.ts +118 -72
  10. package/src/canvas/iframe-entry.ts +20 -0
  11. package/src/canvas/iframe-eval.ts +155 -0
  12. package/src/canvas/iframe-host.ts +131 -5
  13. package/src/canvas/iframe-inline-edit.ts +107 -1
  14. package/src/canvas/iframe-protocol.ts +43 -3
  15. package/src/canvas/iframe-render.ts +18 -0
  16. package/src/collab/collab-session.ts +23 -8
  17. package/src/collab/collab-state.ts +6 -3
  18. package/src/component-props.ts +104 -0
  19. package/src/editor/inline-edit-apply.ts +36 -1
  20. package/src/editor/inline-edit.ts +58 -1
  21. package/src/editor/shortcuts.ts +41 -12
  22. package/src/files/file-ops.ts +14 -0
  23. package/src/files/files.ts +53 -1
  24. package/src/grid/cell-editors.ts +288 -0
  25. package/src/grid/cell-popovers.ts +108 -0
  26. package/src/grid/csv-codec.ts +122 -0
  27. package/src/grid/edit-buffer.ts +490 -0
  28. package/src/grid/grid-controller.ts +417 -0
  29. package/src/grid/grid-layout.ts +83 -0
  30. package/src/grid/grid-open.ts +167 -0
  31. package/src/grid/grid-panel.ts +302 -0
  32. package/src/grid/grid-source.ts +210 -0
  33. package/src/grid/grid-view.ts +367 -0
  34. package/src/grid/schema-columns.ts +261 -0
  35. package/src/grid/sources/connector-source.ts +217 -0
  36. package/src/grid/sources/content-source.ts +542 -0
  37. package/src/grid/sources/csv-file-source.ts +247 -0
  38. package/src/grid/tabulator-tables.d.ts +120 -0
  39. package/src/panels/block-action-bar.ts +8 -3
  40. package/src/panels/chat-panel.ts +98 -0
  41. package/src/panels/data-grid.ts +9 -387
  42. package/src/panels/editors.ts +43 -17
  43. package/src/panels/events-panel.ts +137 -44
  44. package/src/panels/formula-workspace.ts +379 -0
  45. package/src/panels/frontmatter-fields.ts +231 -0
  46. package/src/panels/frontmatter-panel.ts +132 -0
  47. package/src/panels/git-panel.ts +1 -1
  48. package/src/panels/head-panel.ts +11 -175
  49. package/src/panels/properties-panel.ts +154 -144
  50. package/src/panels/right-panel.ts +9 -47
  51. package/src/panels/signals-panel.ts +115 -23
  52. package/src/panels/statement-editor.ts +710 -0
  53. package/src/panels/style-panel.ts +25 -1
  54. package/src/panels/stylebook-panel.ts +0 -2
  55. package/src/panels/tab-bar.ts +175 -6
  56. package/src/panels/tab-strip.ts +62 -6
  57. package/src/panels/toolbar.ts +37 -3
  58. package/src/services/ai-project-tools.ts +541 -0
  59. package/src/services/ai-session-store.ts +28 -0
  60. package/src/services/ai-system-prompt.ts +194 -24
  61. package/src/services/ai-tools.ts +88 -21
  62. package/src/services/automation.ts +16 -0
  63. package/src/services/document-assistant.ts +81 -11
  64. package/src/services/gated-registry.ts +72 -0
  65. package/src/services/live-preview.ts +0 -0
  66. package/src/services/preview-eval.ts +68 -0
  67. package/src/services/project-adoption.ts +31 -0
  68. package/src/site-context.ts +2 -0
  69. package/src/store.ts +4 -0
  70. package/src/studio.ts +83 -52
  71. package/src/tabs/patch-ops.ts +25 -0
  72. package/src/tabs/tab.ts +39 -1
  73. package/src/tabs/transact.ts +60 -13
  74. package/src/types.ts +12 -0
  75. package/src/ui/dynamic-slot.ts +272 -0
  76. package/src/ui/expression-editor.ts +423 -125
  77. package/src/ui/field-row.ts +5 -0
  78. package/src/ui/formula-catalog.ts +557 -0
  79. package/src/ui/formula-chips.ts +216 -0
  80. package/src/ui/formula-palette.ts +211 -0
  81. package/src/ui/layers.ts +40 -0
  82. package/src/ui/media-picker.ts +1 -1
  83. package/src/ui/panel-resize.ts +15 -1
  84. package/src/utils/preview-format.ts +26 -0
  85. package/src/view.ts +4 -4
  86. package/src/workspace/workspace.ts +14 -0
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Grid cell editors and formatters — the lit → Tabulator bridge.
3
+ *
4
+ * Tabulator's editor contract is `(cell, onRendered, success, cancel) => element`: it parents the
5
+ * returned element into the cell and expects `success(value)`/`cancel()` calls. Each factory here
6
+ * lit-renders a control into a host element supplied by grid-view's single sanctioned element
7
+ * factory — this module itself never creates DOM imperatively. Formatters likewise return a
8
+ * rendered element (never an HTML string, which Tabulator would inject as innerHTML).
9
+ *
10
+ * Native inputs (the data-grid precedent) rather than Spectrum controls: cells are 24px
11
+ * micro-controls where SWC shadow focus handling fights Tabulator's editor lifecycle. Rich popover
12
+ * editors (media, relationship pickers) layer on in a later phase.
13
+ */
14
+ import { html, render } from "lit-html";
15
+ import { cellToText, coerceCellInput } from "./schema-columns";
16
+ import type { GridCellValue, GridColumn } from "./grid-source";
17
+
18
+ /** The subset of Tabulator's CellComponent the editors/formatters touch. */
19
+ export interface CellLike {
20
+ getValue: () => unknown;
21
+ }
22
+
23
+ export type CellEditorFn = (
24
+ cell: CellLike,
25
+ onRendered: (fn: () => void) => void,
26
+ success: (value: unknown) => void,
27
+ cancel: () => void,
28
+ ) => HTMLElement;
29
+
30
+ export type CellFormatterFn = (cell: CellLike) => HTMLElement;
31
+
32
+ /** Detached-host factory — grid-view owns the one sanctioned document.createElement. */
33
+ export type HostFactory = (className: string) => HTMLElement;
34
+
35
+ /** Commit/cancel wiring shared by every text-ish editor. */
36
+ function inputEditor(makeHost: HostFactory, column: GridColumn, inputType: string): CellEditorFn {
37
+ return (cell, onRendered, success, cancel) => {
38
+ const host = makeHost("jx-grid-editor");
39
+ let done = false;
40
+ const commit = (raw: string) => {
41
+ if (!done) {
42
+ done = true;
43
+ success(coerceCellInput(raw, column));
44
+ }
45
+ };
46
+ const abort = () => {
47
+ if (!done) {
48
+ done = true;
49
+ cancel();
50
+ }
51
+ };
52
+ const initial =
53
+ column.kind === "date"
54
+ ? cellToText(cell.getValue() as GridCellValue).slice(0, 10)
55
+ : cellToText(cell.getValue() as GridCellValue);
56
+ render(
57
+ html`<input
58
+ class="jx-grid-input"
59
+ type=${inputType}
60
+ .value=${initial}
61
+ @keydown=${(e: KeyboardEvent) => {
62
+ if (e.key === "Enter") {
63
+ commit((e.target as HTMLInputElement).value);
64
+ } else if (e.key === "Escape") {
65
+ abort();
66
+ }
67
+ }}
68
+ @blur=${(e: Event) => commit((e.target as HTMLInputElement).value)}
69
+ />`,
70
+ host,
71
+ );
72
+ onRendered(() => {
73
+ const input = host.querySelector("input");
74
+ input?.focus();
75
+ input?.select();
76
+ });
77
+ return host;
78
+ };
79
+ }
80
+
81
+ function checkboxEditor(makeHost: HostFactory): CellEditorFn {
82
+ return (cell, onRendered, success, cancel) => {
83
+ const host = makeHost("jx-grid-editor");
84
+ let done = false;
85
+ render(
86
+ html`<input
87
+ class="jx-grid-checkbox"
88
+ type="checkbox"
89
+ .checked=${cell.getValue() === true}
90
+ @change=${(e: Event) => {
91
+ if (!done) {
92
+ done = true;
93
+ success((e.target as HTMLInputElement).checked);
94
+ }
95
+ }}
96
+ @keydown=${(e: KeyboardEvent) => {
97
+ if (e.key === "Escape" && !done) {
98
+ done = true;
99
+ cancel();
100
+ } else if (e.key === "Enter" && !done) {
101
+ done = true;
102
+ success((e.target as HTMLInputElement).checked);
103
+ }
104
+ }}
105
+ @blur=${(e: Event) => {
106
+ if (!done) {
107
+ done = true;
108
+ success((e.target as HTMLInputElement).checked);
109
+ }
110
+ }}
111
+ />`,
112
+ host,
113
+ );
114
+ onRendered(() => host.querySelector("input")?.focus());
115
+ return host;
116
+ };
117
+ }
118
+
119
+ function selectEditor(makeHost: HostFactory, column: GridColumn): CellEditorFn {
120
+ const options = Array.isArray(column.schema?.enum)
121
+ ? (column.schema.enum as unknown[]).map(String)
122
+ : [];
123
+ return (cell, onRendered, success, cancel) => {
124
+ const host = makeHost("jx-grid-editor");
125
+ let done = false;
126
+ const current = cellToText(cell.getValue() as GridCellValue);
127
+ render(
128
+ html`<select
129
+ class="jx-grid-select"
130
+ @change=${(e: Event) => {
131
+ if (!done) {
132
+ done = true;
133
+ const { value } = e.target as HTMLSelectElement;
134
+ success(value === "" ? null : value);
135
+ }
136
+ }}
137
+ @keydown=${(e: KeyboardEvent) => {
138
+ if (e.key === "Escape" && !done) {
139
+ done = true;
140
+ cancel();
141
+ }
142
+ }}
143
+ @blur=${() => {
144
+ if (!done) {
145
+ done = true;
146
+ cancel();
147
+ }
148
+ }}
149
+ >
150
+ <option value="" ?selected=${current === ""}>—</option>
151
+ ${options.map(
152
+ (option) =>
153
+ html`<option value=${option} ?selected=${option === current}>${option}</option>`,
154
+ )}
155
+ </select>`,
156
+ host,
157
+ );
158
+ onRendered(() => host.querySelector("select")?.focus());
159
+ return host;
160
+ };
161
+ }
162
+
163
+ /** Pill/chip editor for string arrays: Enter/comma adds, Backspace pops, × removes. */
164
+ function pillEditor(makeHost: HostFactory, column: GridColumn): CellEditorFn {
165
+ return (cell, onRendered, success, cancel) => {
166
+ const host = makeHost("jx-grid-editor jx-grid-pill-editor");
167
+ const initial = coerceCellInput(cell.getValue(), column);
168
+ const chips: string[] = Array.isArray(initial) ? [...initial] : [];
169
+ let done = false;
170
+ const finish = (committed: string[] | null) => {
171
+ if (done) {
172
+ return;
173
+ }
174
+ done = true;
175
+ if (committed) {
176
+ success(committed);
177
+ } else {
178
+ cancel();
179
+ }
180
+ };
181
+ const inputEl = () => host.querySelector("input");
182
+ const addFromInput = (input: HTMLInputElement): boolean => {
183
+ const text = input.value.trim();
184
+ if (text === "") {
185
+ return false;
186
+ }
187
+ chips.push(text);
188
+ input.value = "";
189
+ return true;
190
+ };
191
+ const doRender = () => {
192
+ render(
193
+ html`<span class="jx-grid-pills">
194
+ ${chips.map(
195
+ (chip, i) => html`<span class="jx-grid-chip"
196
+ >${chip}<button
197
+ class="jx-grid-chip-x"
198
+ title="Remove"
199
+ @mousedown=${(e: Event) => e.preventDefault()}
200
+ @click=${() => {
201
+ chips.splice(i, 1);
202
+ doRender();
203
+ inputEl()?.focus();
204
+ }}
205
+ >
206
+ ×
207
+ </button></span
208
+ >`,
209
+ )}
210
+ <input
211
+ class="jx-grid-pill-input"
212
+ placeholder=${chips.length === 0 ? "add…" : ""}
213
+ @keydown=${(e: KeyboardEvent) => {
214
+ const input = e.target as HTMLInputElement;
215
+ if (e.key === "Enter" || e.key === ",") {
216
+ e.preventDefault();
217
+ if (addFromInput(input)) {
218
+ doRender();
219
+ inputEl()?.focus();
220
+ } else if (e.key === "Enter") {
221
+ finish([...chips]);
222
+ }
223
+ } else if (e.key === "Backspace" && input.value === "" && chips.length > 0) {
224
+ chips.pop();
225
+ doRender();
226
+ inputEl()?.focus();
227
+ } else if (e.key === "Escape") {
228
+ finish(null);
229
+ }
230
+ }}
231
+ @blur=${(e: Event) => {
232
+ addFromInput(e.target as HTMLInputElement);
233
+ finish([...chips]);
234
+ }}
235
+ />
236
+ </span>`,
237
+ host,
238
+ );
239
+ };
240
+ doRender();
241
+ onRendered(() => inputEl()?.focus());
242
+ return host;
243
+ };
244
+ }
245
+
246
+ /** Editor factory for a column; undefined means the column is not editable in place. */
247
+ export function editorForColumn(
248
+ column: GridColumn,
249
+ makeHost: HostFactory,
250
+ ): CellEditorFn | undefined {
251
+ if (column.kind === "readonly" || (!column.editable && !column.insertOnly)) {
252
+ return undefined;
253
+ }
254
+ switch (column.kind) {
255
+ case "boolean": {
256
+ return checkboxEditor(makeHost);
257
+ }
258
+ case "enum": {
259
+ return selectEditor(makeHost, column);
260
+ }
261
+ case "date": {
262
+ return inputEditor(makeHost, column, "date");
263
+ }
264
+ case "array": {
265
+ return pillEditor(makeHost, column);
266
+ }
267
+ default: {
268
+ // String/text/number/image/reference — text input + column-typed coercion.
269
+ return inputEditor(makeHost, column, "text");
270
+ }
271
+ }
272
+ }
273
+
274
+ /** Formatter factory — plain-text projection rendered as a real element (innerHTML-safe). */
275
+ export function formatterForColumn(column: GridColumn, makeHost: HostFactory): CellFormatterFn {
276
+ return (cell) => {
277
+ const value = cell.getValue() as GridCellValue;
278
+ const host = makeHost(`jx-grid-cell-content jx-grid-kind-${column.kind}`);
279
+ if (column.kind === "boolean") {
280
+ render(html`${value === true ? "✓" : ""}`, host);
281
+ } else if (column.kind === "array" && Array.isArray(value)) {
282
+ render(html`${value.map((item) => html`<span class="jx-grid-chip">${item}</span>`)}`, host);
283
+ } else {
284
+ render(html`${cellToText(value)}`, host);
285
+ }
286
+ return host;
287
+ };
288
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Popover cell editors — media paths and relationship pickers.
3
+ *
4
+ * These kinds bypass Tabulator's in-cell editor session entirely: their pickers render Spectrum
5
+ * overlays OUTSIDE the cell, which Tabulator's range module treats as an outside interaction and
6
+ * blur-cancels. Instead, a dblclick on the cell opens an anchored layer popover that writes
7
+ * straight through the edit buffer via `commit`.
8
+ */
9
+ import { html, nothing } from "lit-html";
10
+ import { renderPopover } from "../ui/layers";
11
+ import { renderMediaPicker } from "../ui/media-picker";
12
+ import { listCollectionEntryIds } from "./sources/content-source";
13
+ import { cellToText } from "./schema-columns";
14
+ import type { GridCellValue, GridColumn } from "./grid-source";
15
+
16
+ /** Whether this column edits through an anchored popover instead of an in-cell editor. */
17
+ export function hasPopoverEditor(column: GridColumn): boolean {
18
+ return column.kind === "image" || column.kind === "reference";
19
+ }
20
+
21
+ /** Target content-type name of a relationship column (`#/content/<name>` on the schema). */
22
+ export function referenceTargetType(column: GridColumn): string | null {
23
+ const ref = (column.schema as { $ref?: string } | undefined)?.$ref;
24
+ if (typeof ref !== "string" || !ref.startsWith("#/content/")) {
25
+ return null;
26
+ }
27
+ return ref.slice("#/content/".length);
28
+ }
29
+
30
+ export interface CellPopoverArgs {
31
+ /** Viewport rect of the cell the popover anchors to. */
32
+ anchor: { left: number; bottom: number };
33
+ column: GridColumn;
34
+ value: GridCellValue;
35
+ /** Write the new value through the edit buffer (called on every pick/commit). */
36
+ commit: (value: GridCellValue) => void;
37
+ }
38
+
39
+ function popoverShell(
40
+ column: GridColumn,
41
+ anchor: CellPopoverArgs["anchor"],
42
+ body: unknown,
43
+ dismiss: () => void,
44
+ ) {
45
+ const left = Math.max(4, Math.min(anchor.left, window.innerWidth - 340));
46
+ const top = Math.max(4, Math.min(anchor.bottom + 2, window.innerHeight - 200));
47
+ return html`<sp-popover
48
+ open
49
+ class="jx-grid-cell-popover"
50
+ style="position:fixed;z-index:10000;left:${left}px;top:${top}px"
51
+ >
52
+ <div class="jx-grid-cell-popover-body">
53
+ <div class="jx-grid-cell-popover-title">${column.title}</div>
54
+ ${body}
55
+ <div class="jx-grid-cell-popover-actions">
56
+ <sp-button size="s" variant="secondary" @click=${dismiss}>Done</sp-button>
57
+ </div>
58
+ </div>
59
+ </sp-popover>`;
60
+ }
61
+
62
+ /**
63
+ * Open the popover editor for an image or reference cell. Resolves target-entry ids up front for
64
+ * relationship columns so the picker renders complete.
65
+ */
66
+ export async function openCellValuePopover(args: CellPopoverArgs): Promise<void> {
67
+ const { anchor, column, commit, value } = args;
68
+ const current = cellToText(value);
69
+
70
+ let body;
71
+ if (column.kind === "reference") {
72
+ const targetType = referenceTargetType(column);
73
+ const ids = targetType ? await listCollectionEntryIds(targetType) : [];
74
+ body = html`
75
+ ${targetType
76
+ ? html`<div class="jx-grid-cell-popover-hint">Entries of “${targetType}”</div>`
77
+ : nothing}
78
+ <select
79
+ class="jx-grid-select"
80
+ @change=${(e: Event) => {
81
+ const picked = (e.target as HTMLSelectElement).value;
82
+ commit(picked === "" ? null : picked);
83
+ }}
84
+ >
85
+ <option value="" ?selected=${current === ""}>—</option>
86
+ ${ids.map((id) => html`<option value=${id} ?selected=${id === current}>${id}</option>`)}
87
+ </select>
88
+ <input
89
+ class="jx-grid-input"
90
+ placeholder="Custom id…"
91
+ .value=${ids.includes(current) ? "" : current}
92
+ @change=${(e: Event) => {
93
+ const text = (e.target as HTMLInputElement).value.trim();
94
+ commit(text === "" ? null : text);
95
+ }}
96
+ />
97
+ `;
98
+ } else {
99
+ body = renderMediaPicker(column.field, current, (val) => {
100
+ commit(val === "" ? null : val);
101
+ });
102
+ }
103
+
104
+ const handle = renderPopover(
105
+ popoverShell(column, anchor, body, () => handle.dismiss()),
106
+ { dismissOnOutsideClick: true },
107
+ );
108
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Positional CSV codec for the grid editor.
3
+ *
4
+ * RFC 4180 parse/serialize that keeps what the parser extension's object-row loader discards:
5
+ * column order, duplicate/empty headers, untrimmed whitespace, the file's dominant EOL, and whether
6
+ * it ended with a newline. Values stay raw strings — typing lives in schema-columns. Serialization
7
+ * is canonical (quotes only where required), so an edited file is re-emitted minimally quoted;
8
+ * untouched files are never rewritten.
9
+ */
10
+
11
+ export interface CsvDocument {
12
+ headers: string[];
13
+ rows: string[][];
14
+ eol: "\n" | "\r\n";
15
+ trailingNewline: boolean;
16
+ }
17
+
18
+ /** Split CSV text into records of raw fields, honoring quotes across commas and newlines. */
19
+ function splitRecords(text: string): string[][] {
20
+ const records: string[][] = [];
21
+ let fields: string[] = [];
22
+ let field = "";
23
+ let inQuotes = false;
24
+ let fieldStarted = false;
25
+
26
+ const endField = () => {
27
+ fields.push(field);
28
+ field = "";
29
+ fieldStarted = false;
30
+ };
31
+ const endRecord = () => {
32
+ endField();
33
+ // A completely empty line is structure, not data — skip it (common trailing-blank case).
34
+ if (fields.length === 1 && fields[0] === "") {
35
+ fields = [];
36
+ return;
37
+ }
38
+ records.push(fields);
39
+ fields = [];
40
+ };
41
+
42
+ for (let i = 0; i < text.length; i++) {
43
+ const ch = text[i];
44
+ if (inQuotes) {
45
+ if (ch === '"') {
46
+ if (text[i + 1] === '"') {
47
+ field += '"';
48
+ i += 1;
49
+ } else {
50
+ inQuotes = false;
51
+ }
52
+ } else {
53
+ field += ch;
54
+ }
55
+ continue;
56
+ }
57
+ if (ch === '"' && !fieldStarted) {
58
+ inQuotes = true;
59
+ fieldStarted = true;
60
+ } else if (ch === '"') {
61
+ // Quote inside an unquoted field — keep it literal (lenient, matches common parsers).
62
+ field += ch;
63
+ } else if (ch === ",") {
64
+ endField();
65
+ } else if (ch === "\n") {
66
+ endRecord();
67
+ } else if (ch === "\r") {
68
+ if (text[i + 1] === "\n") {
69
+ i += 1;
70
+ }
71
+ endRecord();
72
+ } else {
73
+ field += ch;
74
+ fieldStarted = true;
75
+ }
76
+ }
77
+ if (field !== "" || fields.length > 0) {
78
+ endRecord();
79
+ }
80
+ return records;
81
+ }
82
+
83
+ /**
84
+ * Parse CSV text into headers + positional rows. Rows are padded to the header width; when a data
85
+ * row is wider than the header row, headers are padded with "" so no column is lost.
86
+ */
87
+ export function parseCsv(text: string): CsvDocument {
88
+ const crlf = (text.match(/\r\n/g) ?? []).length;
89
+ const bareLf = (text.match(/(?<!\r)\n/g) ?? []).length;
90
+ const eol: CsvDocument["eol"] = crlf > bareLf ? "\r\n" : "\n";
91
+ const trailingNewline = text.endsWith("\n");
92
+
93
+ const records = splitRecords(text);
94
+ const headers = records[0] ?? [];
95
+ const rows = records.slice(1);
96
+ const width = Math.max(headers.length, ...rows.map((r) => r.length), 0);
97
+ while (headers.length < width) {
98
+ headers.push("");
99
+ }
100
+ for (const row of rows) {
101
+ while (row.length < width) {
102
+ row.push("");
103
+ }
104
+ }
105
+ return { eol, headers, rows, trailingNewline };
106
+ }
107
+
108
+ /** Quote a field iff it contains a delimiter, quote, or newline; double embedded quotes. */
109
+ function encodeField(value: string): string {
110
+ if (/[",\n\r]/.test(value)) {
111
+ return `"${value.replaceAll('"', '""')}"`;
112
+ }
113
+ return value;
114
+ }
115
+
116
+ /** Serialize back to CSV text, preserving the document's EOL flavor and trailing-newline flag. */
117
+ export function serializeCsv(doc: CsvDocument): string {
118
+ const lines = [doc.headers, ...doc.rows].map((fields) =>
119
+ fields.map((field) => encodeField(field)).join(","),
120
+ );
121
+ return lines.join(doc.eol) + (doc.trailingNewline ? doc.eol : "");
122
+ }