@vuu-ui/vuu-shell 0.7.0-debug → 0.7.1-debug

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/esm/index.js CHANGED
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from "react";
3
3
  import cx from "classnames";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  var ConnectionStatusIcon = ({ connectionStatus, className, element = "span", ...props }) => {
6
- const [classBase5, setClassBase] = useState("vuuConnectingStatus");
6
+ const [classBase6, setClassBase] = useState("vuuConnectingStatus");
7
7
  useEffect(() => {
8
8
  switch (connectionStatus) {
9
9
  case "connected":
@@ -24,7 +24,7 @@ var ConnectionStatusIcon = ({ connectionStatus, className, element = "span", ...
24
24
  element,
25
25
  {
26
26
  ...props,
27
- className: cx("vuuStatus vuuIcon", classBase5, className)
27
+ className: cx("vuuStatus vuuIcon", classBase6, className)
28
28
  }
29
29
  );
30
30
  return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "vuuStatus-container salt-theme", children: [
@@ -65,7 +65,7 @@ var DensitySwitch = ({
65
65
  };
66
66
 
67
67
  // src/feature/Feature.tsx
68
- import React3, { Suspense } from "react";
68
+ import React3, { Suspense, useEffect as useEffect2 } from "react";
69
69
  import { registerComponent } from "@vuu-ui/vuu-layout";
70
70
 
71
71
  // src/feature/ErrorBoundary.jsx
@@ -76,11 +76,11 @@ var ErrorBoundary = class extends React2.Component {
76
76
  super(props);
77
77
  this.state = { errorMessage: null };
78
78
  }
79
- static getDerivedStateFromError(error) {
80
- return { errorMessage: error.message };
79
+ static getDerivedStateFromError(error2) {
80
+ return { errorMessage: error2.message };
81
81
  }
82
- componentDidCatch(error, errorInfo) {
83
- console.log(error, errorInfo);
82
+ componentDidCatch(error2, errorInfo) {
83
+ console.log(error2, errorInfo);
84
84
  }
85
85
  render() {
86
86
  if (this.state.errorMessage) {
@@ -99,18 +99,45 @@ var Loader = () => /* @__PURE__ */ jsx4("div", { className: "hwLoader", children
99
99
 
100
100
  // src/feature/Feature.tsx
101
101
  import { jsx as jsx5 } from "react/jsx-runtime";
102
+ var componentsMap = /* @__PURE__ */ new Map();
103
+ var useCachedFeature = (url) => {
104
+ useEffect2(
105
+ () => () => {
106
+ componentsMap.delete(url);
107
+ },
108
+ [url]
109
+ );
110
+ if (!componentsMap.has(url)) {
111
+ componentsMap.set(
112
+ url,
113
+ React3.lazy(() => import(
114
+ /* @vite-ignore */
115
+ url
116
+ ))
117
+ );
118
+ }
119
+ return componentsMap.get(url);
120
+ };
102
121
  function RawFeature({
103
122
  url,
104
123
  css,
105
124
  params,
106
125
  ...props
107
126
  }) {
127
+ console.log("Feature render", { css, url, props });
128
+ useEffect2(() => {
129
+ console.log("%cFeature mount", "color: green;");
130
+ return () => {
131
+ console.log("%cFeature unmount", "color:red;");
132
+ };
133
+ }, []);
108
134
  if (css) {
109
135
  import(
110
136
  /* @vite-ignore */
111
137
  css
112
138
  ).then(
113
139
  (cssModule) => {
140
+ console.log("%cInject Styles", "color: blue;font-weight: bold");
114
141
  document.adoptedStyleSheets = [
115
142
  ...document.adoptedStyleSheets,
116
143
  cssModule.default
@@ -118,10 +145,7 @@ function RawFeature({
118
145
  }
119
146
  );
120
147
  }
121
- const LazyFeature = React3.lazy(() => import(
122
- /* @vite-ignore */
123
- url
124
- ));
148
+ const LazyFeature = useCachedFeature(url);
125
149
  return /* @__PURE__ */ jsx5(ErrorBoundary, { children: /* @__PURE__ */ jsx5(Suspense, { fallback: /* @__PURE__ */ jsx5(Loader, {}), children: /* @__PURE__ */ jsx5(LazyFeature, { ...props, ...params }) }) });
126
150
  }
127
151
  var Feature = React3.memo(RawFeature);
@@ -187,19 +211,289 @@ var logout = (loginUrl) => {
187
211
  redirectToLogin(loginUrl);
188
212
  };
189
213
 
214
+ // src/session-editing-form/SessionEditingForm.tsx
215
+ import {
216
+ useCallback as useCallback2,
217
+ useEffect as useEffect3,
218
+ useMemo,
219
+ useRef,
220
+ useState as useState3
221
+ } from "react";
222
+ import cx3 from "classnames";
223
+ import { useIdMemo } from "@salt-ds/core";
224
+ import { Button as Button2 } from "@salt-ds/core";
225
+ import {
226
+ isErrorResponse,
227
+ RemoteDataSource
228
+ } from "@vuu-ui/vuu-data";
229
+ import {
230
+ buildColumnMap,
231
+ isValidNumber,
232
+ shallowEquals
233
+ } from "@vuu-ui/vuu-utils";
234
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
235
+ var classBase3 = "vuuSessionEditingForm";
236
+ var getField = (fields, name) => {
237
+ const field = fields.find((f) => f.name === name);
238
+ if (field) {
239
+ return field;
240
+ } else {
241
+ throw Error(`SessionEditingForm, no field '${name}' found`);
242
+ }
243
+ };
244
+ var getFieldNameAndValue = (evt) => {
245
+ const {
246
+ dataset: { field },
247
+ value
248
+ } = evt.target;
249
+ if (field === void 0) {
250
+ throw Error("SessionEditingForm, form field has no field name");
251
+ }
252
+ return [field, value];
253
+ };
254
+ var Status = {
255
+ uninitialised: 0,
256
+ unchanged: 1,
257
+ changed: 2,
258
+ invalid: 3
259
+ };
260
+ function getTypedValue(value, type, throwIfUndefined = false) {
261
+ switch (type) {
262
+ case "int":
263
+ case "long": {
264
+ const typedValue = parseInt(value, 10);
265
+ if (isValidNumber(typedValue)) {
266
+ return typedValue;
267
+ } else if (throwIfUndefined) {
268
+ throw Error("SessionEditingForm getTypedValue");
269
+ } else {
270
+ return void 0;
271
+ }
272
+ }
273
+ case "double": {
274
+ const typedValue = parseFloat(value);
275
+ if (isValidNumber(typedValue)) {
276
+ return typedValue;
277
+ }
278
+ return void 0;
279
+ }
280
+ case "boolean":
281
+ return value === "true" ? true : false;
282
+ default:
283
+ return value;
284
+ }
285
+ }
286
+ var getDataSource = (dataSource, schema) => {
287
+ if (dataSource) {
288
+ return dataSource;
289
+ } else if (schema) {
290
+ return new RemoteDataSource({
291
+ bufferSize: 0,
292
+ table: schema.table,
293
+ columns: schema.columns.map((col) => col.name)
294
+ });
295
+ } else {
296
+ throw Error(
297
+ "SessionEditingForm: either a DataSource or a TableSchema must be provided"
298
+ );
299
+ }
300
+ };
301
+ var SessionEditingForm = ({
302
+ className,
303
+ config: { fields, key: keyField },
304
+ dataSource: dataSourceProp,
305
+ id: idProp,
306
+ onClose,
307
+ schema,
308
+ ...htmlAttributes
309
+ }) => {
310
+ const [values, setValues] = useState3();
311
+ const [errorMessage, setErrorMessage] = useState3("");
312
+ const formContentRef = useRef(null);
313
+ const initialDataRef = useRef();
314
+ const dataStatusRef = useRef(Status.uninitialised);
315
+ const dataSource = useMemo(() => {
316
+ const applyServerData = (data) => {
317
+ if (columnMap) {
318
+ const values2 = {};
319
+ for (const column of dataSource.columns) {
320
+ values2[column] = data[columnMap[column]];
321
+ }
322
+ if (dataStatusRef.current === Status.uninitialised) {
323
+ dataStatusRef.current = Status.unchanged;
324
+ initialDataRef.current = values2;
325
+ }
326
+ setValues(values2);
327
+ }
328
+ };
329
+ const ds = getDataSource(dataSourceProp, schema);
330
+ const columnMap = buildColumnMap(ds.columns);
331
+ ds.subscribe({ range: { from: 0, to: 5 } }, (message) => {
332
+ if (message.type === "viewport-update" && message.rows) {
333
+ if (dataStatusRef.current === Status.uninitialised) {
334
+ applyServerData(message.rows[0]);
335
+ } else {
336
+ console.log("what do we do with server updates");
337
+ }
338
+ }
339
+ });
340
+ return ds;
341
+ }, [dataSourceProp, schema]);
342
+ const id = useIdMemo(idProp);
343
+ const handleChange = useCallback2(
344
+ (evt) => {
345
+ const [field, value] = getFieldNameAndValue(evt);
346
+ const { type } = getField(fields, field);
347
+ const typedValue = getTypedValue(value, type);
348
+ setValues((values2 = {}) => {
349
+ const newValues = {
350
+ ...values2,
351
+ [field]: typedValue
352
+ };
353
+ const notUpdated = shallowEquals(newValues, initialDataRef.current);
354
+ dataStatusRef.current = notUpdated ? Status.unchanged : typedValue !== void 0 ? Status.changed : Status.invalid;
355
+ return newValues;
356
+ });
357
+ },
358
+ [fields]
359
+ );
360
+ const handleBlur = useCallback2(
361
+ (evt) => {
362
+ const [field, value] = getFieldNameAndValue(evt);
363
+ const { type } = getField(fields, field);
364
+ console.log("BLUR", {
365
+ keyField
366
+ });
367
+ const rowKey = values == null ? void 0 : values[keyField];
368
+ const typedValue = getTypedValue(value, type, true);
369
+ if (typeof rowKey === "string") {
370
+ dataSource.menuRpcCall({
371
+ rowKey,
372
+ field,
373
+ value: typedValue,
374
+ type: "VP_EDIT_CELL_RPC"
375
+ });
376
+ }
377
+ },
378
+ [dataSource, fields, keyField, values]
379
+ );
380
+ const handleSubmit = useCallback2(async () => {
381
+ const response = await dataSource.menuRpcCall({
382
+ type: "VP_EDIT_SUBMIT_FORM_RPC"
383
+ });
384
+ if (isErrorResponse(response)) {
385
+ setErrorMessage(response.error);
386
+ }
387
+ }, [dataSource]);
388
+ const handleKeyDown = useCallback2(
389
+ (evt) => {
390
+ if (evt.key === "Enter" && dataStatusRef.current === Status.changed) {
391
+ handleSubmit();
392
+ }
393
+ },
394
+ [handleSubmit]
395
+ );
396
+ const handleCancel = useCallback2(() => {
397
+ onClose();
398
+ }, [onClose]);
399
+ const getFormControl = (field) => {
400
+ var _a;
401
+ const value = String((_a = values == null ? void 0 : values[field.name]) != null ? _a : "");
402
+ if (field.readonly || field.name === keyField) {
403
+ return /* @__PURE__ */ jsx7("div", { className: `${classBase3}-fieldValue vuuReadOnly`, children: value });
404
+ } else {
405
+ return /* @__PURE__ */ jsx7(
406
+ "input",
407
+ {
408
+ className: `${classBase3}-fieldValue`,
409
+ "data-field": field.name,
410
+ onBlur: handleBlur,
411
+ onChange: handleChange,
412
+ type: "text",
413
+ value,
414
+ id: `${id}-input-${field.name}`
415
+ }
416
+ );
417
+ }
418
+ };
419
+ useEffect3(() => {
420
+ if (formContentRef.current) {
421
+ const firstInput = formContentRef.current.querySelector(
422
+ "input"
423
+ );
424
+ if (firstInput) {
425
+ setTimeout(() => {
426
+ firstInput.focus();
427
+ console.log("select item");
428
+ firstInput.select();
429
+ }, 100);
430
+ }
431
+ }
432
+ }, []);
433
+ const isDirty = dataStatusRef.current === Status.changed;
434
+ return /* @__PURE__ */ jsxs4("div", { ...htmlAttributes, className: cx3(classBase3, className), children: [
435
+ errorMessage ? /* @__PURE__ */ jsx7(
436
+ "div",
437
+ {
438
+ className: `${classBase3}-errorBanner`,
439
+ "data-icon": "error",
440
+ title: errorMessage,
441
+ children: "Error, edit(s) not saved"
442
+ }
443
+ ) : void 0,
444
+ /* @__PURE__ */ jsx7(
445
+ "div",
446
+ {
447
+ className: `${classBase3}-content`,
448
+ ref: formContentRef,
449
+ onKeyDown: handleKeyDown,
450
+ children: fields.map((field) => {
451
+ var _a;
452
+ return /* @__PURE__ */ jsxs4("div", { className: `${classBase3}-field`, children: [
453
+ /* @__PURE__ */ jsx7(
454
+ "label",
455
+ {
456
+ className: cx3(`${classBase3}-fieldLabel`, {
457
+ [`${classBase3}-required`]: field.required
458
+ }),
459
+ htmlFor: `${id}-input-${field.name}`,
460
+ children: (_a = field == null ? void 0 : field.label) != null ? _a : field.description
461
+ }
462
+ ),
463
+ getFormControl(field)
464
+ ] }, field.name);
465
+ })
466
+ }
467
+ ),
468
+ /* @__PURE__ */ jsxs4("div", { className: `${classBase3}-buttonbar salt-theme salt-density-high`, children: [
469
+ /* @__PURE__ */ jsx7(
470
+ Button2,
471
+ {
472
+ type: "submit",
473
+ variant: "cta",
474
+ disabled: !isDirty,
475
+ onClick: handleSubmit,
476
+ children: "Submit"
477
+ }
478
+ ),
479
+ /* @__PURE__ */ jsx7(Button2, { variant: "secondary", onClick: handleCancel, children: "Cancel" })
480
+ ] })
481
+ ] });
482
+ };
483
+
190
484
  // src/shell.tsx
191
485
  import { connectToServer } from "@vuu-ui/vuu-data";
192
- import cx5 from "classnames";
486
+ import cx6 from "classnames";
193
487
  import {
194
- useCallback as useCallback6,
195
- useEffect as useEffect4,
196
- useRef,
197
- useState as useState5
488
+ useCallback as useCallback7,
489
+ useEffect as useEffect6,
490
+ useRef as useRef2,
491
+ useState as useState6
198
492
  } from "react";
199
493
 
200
494
  // src/ShellContextProvider.tsx
201
495
  import { createContext, useContext } from "react";
202
- import { jsx as jsx7 } from "react/jsx-runtime";
496
+ import { jsx as jsx8 } from "react/jsx-runtime";
203
497
  var defaultConfig = {};
204
498
  var ShellContext = createContext(defaultConfig);
205
499
  var Provider = ({
@@ -211,53 +505,106 @@ var Provider = ({
211
505
  ...inheritedContext,
212
506
  ...context
213
507
  };
214
- return /* @__PURE__ */ jsx7(ShellContext.Provider, { value: mergedContext, children });
508
+ return /* @__PURE__ */ jsx8(ShellContext.Provider, { value: mergedContext, children });
215
509
  };
216
510
  var ShellContextProvider = ({
217
511
  children,
218
512
  value
219
513
  }) => {
220
- return /* @__PURE__ */ jsx7(ShellContext.Consumer, { children: (context) => /* @__PURE__ */ jsx7(Provider, { context: value, inheritedContext: context, children }) });
514
+ return /* @__PURE__ */ jsx8(ShellContext.Consumer, { children: (context) => /* @__PURE__ */ jsx8(Provider, { context: value, inheritedContext: context, children }) });
221
515
  };
222
516
  var useShellContext = () => {
223
517
  return useContext(ShellContext);
224
518
  };
225
519
 
226
- // src/use-layout-config.js
227
- import { useCallback as useCallback2, useEffect as useEffect2, useState as useState3 } from "react";
228
- var useLayoutConfig = (user, defaultLayout) => {
229
- const [layout, _setLayout] = useState3(defaultLayout);
520
+ // src/layout-config/use-layout-config.ts
521
+ import { useCallback as useCallback3, useEffect as useEffect4, useState as useState4 } from "react";
522
+
523
+ // src/layout-config/local-config.ts
524
+ var loadLocalConfig = (saveUrl, user, id = "latest") => new Promise((resolve, reject) => {
525
+ console.log(
526
+ `load local config at ${saveUrl} for user ${user.username}, id ${id}`
527
+ );
528
+ const data = localStorage.getItem(saveUrl);
529
+ if (data) {
530
+ const layout = JSON.parse(data);
531
+ resolve(layout);
532
+ } else {
533
+ reject();
534
+ }
535
+ });
536
+ var saveLocalConfig = (saveUrl, user, data) => new Promise((resolve, reject) => {
537
+ try {
538
+ localStorage.setItem(saveUrl, JSON.stringify(data));
539
+ resolve(void 0);
540
+ } catch {
541
+ reject();
542
+ }
543
+ });
544
+
545
+ // src/layout-config/remote-config.ts
546
+ var loadRemoteConfig = (saveUrl, user, id = "latest") => new Promise((resolve, reject) => {
547
+ fetch(`${saveUrl}/${user.username}/${id}`, {}).then((response) => {
548
+ if (response.ok) {
549
+ resolve(response.json());
550
+ } else {
551
+ reject(void 0);
552
+ }
553
+ }).catch(() => {
554
+ reject(void 0);
555
+ });
556
+ });
557
+ var saveRemoteConfig = (saveUrl, user, data) => new Promise((resolve, reject) => {
558
+ fetch(`${saveUrl}/${user.username}`, {
559
+ method: "POST",
560
+ headers: {
561
+ "Content-Type": "application/json"
562
+ },
563
+ body: JSON.stringify(data)
564
+ }).then((response) => {
565
+ if (response.ok) {
566
+ resolve(void 0);
567
+ } else {
568
+ reject();
569
+ }
570
+ });
571
+ });
572
+
573
+ // src/layout-config/use-layout-config.ts
574
+ var useLayoutConfig = ({
575
+ saveLocation,
576
+ saveUrl = "api/vui",
577
+ user,
578
+ defaultLayout
579
+ }) => {
580
+ const [layout, _setLayout] = useState4(defaultLayout);
581
+ const usingRemote = saveLocation === "remote";
582
+ const loadConfig = usingRemote ? loadRemoteConfig : loadLocalConfig;
583
+ const saveConfig = usingRemote ? saveRemoteConfig : saveLocalConfig;
230
584
  const setLayout = (layout2) => {
231
585
  _setLayout(layout2);
232
586
  };
233
- const load = useCallback2(
587
+ const load = useCallback3(
234
588
  async (id = "latest") => {
235
- fetch(`api/vui/${user.username}/${id}`, {}).then((response) => {
236
- return response.ok ? response.json() : defaultLayout;
237
- }).then(setLayout).catch(() => {
589
+ try {
590
+ const layout2 = await loadConfig(saveUrl, user, id);
591
+ setLayout(layout2);
592
+ } catch {
238
593
  setLayout(defaultLayout);
239
- });
594
+ }
240
595
  },
241
- [defaultLayout, user.username]
596
+ [defaultLayout, loadConfig, saveUrl, user]
242
597
  );
243
- useEffect2(() => {
598
+ useEffect4(() => {
244
599
  load();
245
600
  }, [load]);
246
- const saveData = useCallback2(
601
+ const saveData = useCallback3(
247
602
  (data) => {
248
- fetch(`api/vui/${user.username}`, {
249
- method: "POST",
250
- headers: {
251
- "Content-Type": "application/json"
252
- },
253
- body: JSON.stringify(data)
254
- }).then((response) => {
255
- return response.ok ? response.json() : defaultLayout;
256
- });
603
+ saveConfig(saveUrl, user, data);
257
604
  },
258
- [defaultLayout, user]
605
+ [saveConfig, saveUrl, user]
259
606
  );
260
- const loadLayoutById = useCallback2(
607
+ const loadLayoutById = useCallback3(
261
608
  (id) => {
262
609
  load(id);
263
610
  },
@@ -265,7 +612,6 @@ var useLayoutConfig = (user, defaultLayout) => {
265
612
  );
266
613
  return [layout, saveData, loadLayoutById];
267
614
  };
268
- var use_layout_config_default = useLayoutConfig;
269
615
 
270
616
  // src/shell.tsx
271
617
  import {
@@ -278,23 +624,23 @@ import {
278
624
  } from "@vuu-ui/vuu-layout";
279
625
 
280
626
  // src/app-header/AppHeader.tsx
281
- import { useCallback as useCallback5 } from "react";
627
+ import { useCallback as useCallback6 } from "react";
282
628
 
283
629
  // src/user-profile/UserProfile.tsx
284
- import { Button as Button3 } from "@salt-ds/core";
630
+ import { Button as Button4 } from "@salt-ds/core";
285
631
  import { DropdownBase } from "@heswell/salt-lab";
286
632
  import { UserSolidIcon } from "@salt-ds/icons";
287
633
 
288
634
  // src/user-profile/UserPanel.tsx
289
635
  import { formatDate } from "@vuu-ui/vuu-utils";
290
636
  import { List, ListItem } from "@heswell/salt-lab";
291
- import { Button as Button2 } from "@salt-ds/core";
637
+ import { Button as Button3 } from "@salt-ds/core";
292
638
  import { ExportIcon } from "@salt-ds/icons";
293
639
  import {
294
640
  forwardRef,
295
- useCallback as useCallback3,
296
- useEffect as useEffect3,
297
- useState as useState4
641
+ useCallback as useCallback4,
642
+ useEffect as useEffect5,
643
+ useState as useState5
298
644
  } from "react";
299
645
 
300
646
  // src/get-layout-history.ts
@@ -308,16 +654,16 @@ var getLayoutHistory = async (user) => {
308
654
  };
309
655
 
310
656
  // src/user-profile/UserPanel.tsx
311
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
657
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
312
658
  var byLastUpdate = ({ lastUpdate: l1 }, { lastUpdate: l2 }) => {
313
659
  return l2 === l1 ? 0 : l2 < l1 ? -1 : 1;
314
660
  };
315
661
  var HistoryListItem = (props) => {
316
- return /* @__PURE__ */ jsx8(ListItem, { ...props });
662
+ return /* @__PURE__ */ jsx9(ListItem, { ...props });
317
663
  };
318
664
  var UserPanel = forwardRef(function UserPanel2({ loginUrl, onNavigate, user, layoutId = "latest" }, forwardedRef) {
319
- const [history, setHistory] = useState4([]);
320
- useEffect3(() => {
665
+ const [history, setHistory] = useState5([]);
666
+ useEffect5(() => {
321
667
  async function getHistory() {
322
668
  const history2 = await getLayoutHistory(user);
323
669
  const sortedHistory = history2.filter((item) => item.id !== "latest").sort(byLastUpdate).map(({ id, lastUpdate }) => ({
@@ -330,7 +676,7 @@ var UserPanel = forwardRef(function UserPanel2({ loginUrl, onNavigate, user, lay
330
676
  }
331
677
  getHistory();
332
678
  }, [user]);
333
- const handleHisorySelected = useCallback3(
679
+ const handleHisorySelected = useCallback4(
334
680
  (evt, selected2) => {
335
681
  if (selected2) {
336
682
  onNavigate(selected2.id);
@@ -338,12 +684,12 @@ var UserPanel = forwardRef(function UserPanel2({ loginUrl, onNavigate, user, lay
338
684
  },
339
685
  [onNavigate]
340
686
  );
341
- const handleLogout = useCallback3(() => {
687
+ const handleLogout = useCallback4(() => {
342
688
  logout(loginUrl);
343
689
  }, [loginUrl]);
344
690
  const selected = history.length === 0 ? null : layoutId === "latest" ? history[0] : history.find((i) => i.id === layoutId);
345
- return /* @__PURE__ */ jsxs4("div", { className: "vuuUserPanel", ref: forwardedRef, children: [
346
- /* @__PURE__ */ jsx8(
691
+ return /* @__PURE__ */ jsxs5("div", { className: "vuuUserPanel", ref: forwardedRef, children: [
692
+ /* @__PURE__ */ jsx9(
347
693
  List,
348
694
  {
349
695
  ListItem: HistoryListItem,
@@ -353,15 +699,15 @@ var UserPanel = forwardRef(function UserPanel2({ loginUrl, onNavigate, user, lay
353
699
  source: history
354
700
  }
355
701
  ),
356
- /* @__PURE__ */ jsx8("div", { className: "vuuUserPanel-buttonBar", children: /* @__PURE__ */ jsxs4(Button2, { "aria-label": "logout", onClick: handleLogout, children: [
357
- /* @__PURE__ */ jsx8(ExportIcon, {}),
702
+ /* @__PURE__ */ jsx9("div", { className: "vuuUserPanel-buttonBar", children: /* @__PURE__ */ jsxs5(Button3, { "aria-label": "logout", onClick: handleLogout, children: [
703
+ /* @__PURE__ */ jsx9(ExportIcon, {}),
358
704
  " Logout"
359
705
  ] }) })
360
706
  ] });
361
707
  });
362
708
 
363
709
  // src/user-profile/UserProfile.tsx
364
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
710
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
365
711
  var UserProfile = ({
366
712
  layoutId,
367
713
  loginUrl,
@@ -371,9 +717,9 @@ var UserProfile = ({
371
717
  const handleNavigate = (id) => {
372
718
  onNavigate(id);
373
719
  };
374
- return /* @__PURE__ */ jsxs5(DropdownBase, { className: "vuuUserProfile", placement: "bottom-end", children: [
375
- /* @__PURE__ */ jsx9(Button3, { variant: "secondary", children: /* @__PURE__ */ jsx9(UserSolidIcon, {}) }),
376
- /* @__PURE__ */ jsx9(
720
+ return /* @__PURE__ */ jsxs6(DropdownBase, { className: "vuuUserProfile", placement: "bottom-end", children: [
721
+ /* @__PURE__ */ jsx10(Button4, { variant: "secondary", children: /* @__PURE__ */ jsx10(UserSolidIcon, {}) }),
722
+ /* @__PURE__ */ jsx10(
377
723
  UserPanel,
378
724
  {
379
725
  layoutId,
@@ -390,11 +736,11 @@ import {
390
736
  ToggleButton,
391
737
  ToggleButtonGroup
392
738
  } from "@heswell/salt-lab";
393
- import cx3 from "classnames";
739
+ import cx4 from "classnames";
394
740
  import { useControlled } from "@salt-ds/core";
395
- import { useCallback as useCallback4 } from "react";
396
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
397
- var classBase3 = "vuuThemeSwitch";
741
+ import { useCallback as useCallback5 } from "react";
742
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
743
+ var classBase4 = "vuuThemeSwitch";
398
744
  var modes = ["light", "dark"];
399
745
  var ThemeSwitch = ({
400
746
  className: classNameProp,
@@ -410,7 +756,7 @@ var ThemeSwitch = ({
410
756
  state: "mode"
411
757
  });
412
758
  const selectedIndex = modes.indexOf(mode);
413
- const handleChangeSecondary = useCallback4(
759
+ const handleChangeSecondary = useCallback5(
414
760
  (_evt, index) => {
415
761
  const mode2 = modes[index];
416
762
  setMode(mode2);
@@ -418,8 +764,8 @@ var ThemeSwitch = ({
418
764
  },
419
765
  [onChange, setMode]
420
766
  );
421
- const className = cx3(classBase3, classNameProp);
422
- return /* @__PURE__ */ jsxs6(
767
+ const className = cx4(classBase4, classNameProp);
768
+ return /* @__PURE__ */ jsxs7(
423
769
  ToggleButtonGroup,
424
770
  {
425
771
  className,
@@ -427,7 +773,7 @@ var ThemeSwitch = ({
427
773
  onChange: handleChangeSecondary,
428
774
  selectedIndex,
429
775
  children: [
430
- /* @__PURE__ */ jsx10(
776
+ /* @__PURE__ */ jsx11(
431
777
  ToggleButton,
432
778
  {
433
779
  "aria-label": "alert",
@@ -435,7 +781,7 @@ var ThemeSwitch = ({
435
781
  "data-icon": "light"
436
782
  }
437
783
  ),
438
- /* @__PURE__ */ jsx10(
784
+ /* @__PURE__ */ jsx11(
439
785
  ToggleButton,
440
786
  {
441
787
  "aria-label": "home",
@@ -449,9 +795,9 @@ var ThemeSwitch = ({
449
795
  };
450
796
 
451
797
  // src/app-header/AppHeader.tsx
452
- import cx4 from "classnames";
453
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
454
- var classBase4 = "vuuAppHeader";
798
+ import cx5 from "classnames";
799
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
800
+ var classBase5 = "vuuAppHeader";
455
801
  var AppHeader = ({
456
802
  className: classNameProp,
457
803
  layoutId,
@@ -462,14 +808,14 @@ var AppHeader = ({
462
808
  user,
463
809
  ...htmlAttributes
464
810
  }) => {
465
- const className = cx4(classBase4, classNameProp);
466
- const handleSwitchTheme = useCallback5(
811
+ const className = cx5(classBase5, classNameProp);
812
+ const handleSwitchTheme = useCallback6(
467
813
  (mode) => onSwitchTheme == null ? void 0 : onSwitchTheme(mode),
468
814
  [onSwitchTheme]
469
815
  );
470
- return /* @__PURE__ */ jsxs7("header", { className, ...htmlAttributes, children: [
471
- /* @__PURE__ */ jsx11(ThemeSwitch, { defaultMode: themeMode, onChange: handleSwitchTheme }),
472
- /* @__PURE__ */ jsx11(
816
+ return /* @__PURE__ */ jsxs8("header", { className, ...htmlAttributes, children: [
817
+ /* @__PURE__ */ jsx12(ThemeSwitch, { defaultMode: themeMode, onChange: handleSwitchTheme }),
818
+ /* @__PURE__ */ jsx12(
473
819
  UserProfile,
474
820
  {
475
821
  layoutId,
@@ -482,7 +828,9 @@ var AppHeader = ({
482
828
  };
483
829
 
484
830
  // src/shell.tsx
485
- import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
831
+ import { logger } from "@vuu-ui/vuu-utils";
832
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
833
+ var { error } = logger("Shell");
486
834
  var warningLayout = {
487
835
  type: "View",
488
836
  props: {
@@ -503,25 +851,32 @@ var Shell = ({
503
851
  defaultLayout = warningLayout,
504
852
  leftSidePanel,
505
853
  loginUrl,
854
+ saveLocation = "remote",
855
+ saveUrl,
506
856
  serverUrl,
507
857
  user,
508
858
  ...htmlAttributes
509
859
  }) => {
510
- const rootRef = useRef(null);
511
- const paletteView = useRef(null);
512
- const [open, setOpen] = useState5(false);
513
- const layoutId = useRef("latest");
514
- const [layout, setLayoutConfig, loadLayoutById] = use_layout_config_default(
515
- user,
516
- defaultLayout
517
- );
518
- const handleLayoutChange = useCallback6(
860
+ const rootRef = useRef2(null);
861
+ const paletteView = useRef2(null);
862
+ const [open, setOpen] = useState6(false);
863
+ const layoutId = useRef2("latest");
864
+ const [layout, saveLayoutConfig, loadLayoutById] = useLayoutConfig({
865
+ defaultLayout,
866
+ saveLocation,
867
+ user
868
+ });
869
+ const handleLayoutChange = useCallback7(
519
870
  (layout2) => {
520
- setLayoutConfig(layout2);
871
+ try {
872
+ saveLayoutConfig(layout2);
873
+ } catch {
874
+ error == null ? void 0 : error("Failed to save layout");
875
+ }
521
876
  },
522
- [setLayoutConfig]
877
+ [saveLayoutConfig]
523
878
  );
524
- const handleSwitchTheme = useCallback6((mode) => {
879
+ const handleSwitchTheme = useCallback7((mode) => {
525
880
  if (rootRef.current) {
526
881
  rootRef.current.dataset.mode = mode;
527
882
  }
@@ -533,14 +888,14 @@ var Shell = ({
533
888
  setOpen(!open);
534
889
  }
535
890
  };
536
- const handleNavigate = useCallback6(
891
+ const handleNavigate = useCallback7(
537
892
  (id) => {
538
893
  layoutId.current = id;
539
894
  loadLayoutById(id);
540
895
  },
541
896
  [loadLayoutById]
542
897
  );
543
- useEffect4(() => {
898
+ useEffect6(() => {
544
899
  if (serverUrl && user.token) {
545
900
  connectToServer({
546
901
  authToken: user.token,
@@ -553,7 +908,7 @@ var Shell = ({
553
908
  const drawers = [];
554
909
  if (leftSidePanel) {
555
910
  drawers.push(
556
- /* @__PURE__ */ jsx12(
911
+ /* @__PURE__ */ jsx13(
557
912
  Drawer,
558
913
  {
559
914
  onClick: handleDrawerClick,
@@ -563,7 +918,7 @@ var Shell = ({
563
918
  peekaboo: true,
564
919
  sizeOpen: 200,
565
920
  toggleButton: "end",
566
- children: /* @__PURE__ */ jsx12(
921
+ children: /* @__PURE__ */ jsx13(
567
922
  View,
568
923
  {
569
924
  className: "vuuShell-palette",
@@ -581,7 +936,7 @@ var Shell = ({
581
936
  }
582
937
  return drawers;
583
938
  };
584
- const className = cx5(
939
+ const className = cx6(
585
940
  "vuuShell",
586
941
  classNameProp,
587
942
  "salt-theme",
@@ -589,21 +944,21 @@ var Shell = ({
589
944
  );
590
945
  return (
591
946
  // ShellContext TBD
592
- /* @__PURE__ */ jsxs8(ShellContextProvider, { value: void 0, children: [
593
- /* @__PURE__ */ jsx12(LayoutProvider, { layout, onLayoutChange: handleLayoutChange, children: /* @__PURE__ */ jsx12(
947
+ /* @__PURE__ */ jsxs9(ShellContextProvider, { value: void 0, children: [
948
+ /* @__PURE__ */ jsx13(LayoutProvider, { layout, onLayoutChange: handleLayoutChange, children: /* @__PURE__ */ jsx13(
594
949
  DraggableLayout,
595
950
  {
596
951
  className,
597
952
  "data-mode": "light",
598
953
  ref: rootRef,
599
954
  ...htmlAttributes,
600
- children: /* @__PURE__ */ jsxs8(
955
+ children: /* @__PURE__ */ jsxs9(
601
956
  Flexbox,
602
957
  {
603
958
  className: "App",
604
959
  style: { flexDirection: "column", height: "100%", width: "100%" },
605
960
  children: [
606
- /* @__PURE__ */ jsx12(
961
+ /* @__PURE__ */ jsx13(
607
962
  AppHeader,
608
963
  {
609
964
  layoutId: layoutId.current,
@@ -613,8 +968,8 @@ var Shell = ({
613
968
  onSwitchTheme: handleSwitchTheme
614
969
  }
615
970
  ),
616
- /* @__PURE__ */ jsx12(DockLayout, { style: { flex: 1 }, children: getDrawers().concat(
617
- /* @__PURE__ */ jsx12(
971
+ /* @__PURE__ */ jsx13(DockLayout, { style: { flex: 1 }, children: getDrawers().concat(
972
+ /* @__PURE__ */ jsx13(
618
973
  DraggableLayout,
619
974
  {
620
975
  dropTarget: true,
@@ -640,8 +995,8 @@ import {
640
995
  cloneElement,
641
996
  useContext as useContext2
642
997
  } from "react";
643
- import cx6 from "classnames";
644
- import { jsx as jsx13 } from "react/jsx-runtime";
998
+ import cx7 from "classnames";
999
+ import { jsx as jsx14 } from "react/jsx-runtime";
645
1000
  var DEFAULT_DENSITY2 = "medium";
646
1001
  var DEFAULT_THEME = "salt-theme";
647
1002
  var DEFAULT_THEME_MODE = "light";
@@ -654,7 +1009,7 @@ var createThemedChildren = (children, theme, themeMode, density) => {
654
1009
  var _a;
655
1010
  if (isValidElement(children)) {
656
1011
  return cloneElement(children, {
657
- className: cx6(
1012
+ className: cx7(
658
1013
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
659
1014
  (_a = children.props) == null ? void 0 : _a.className,
660
1015
  theme,
@@ -694,7 +1049,7 @@ var ThemeProvider = ({
694
1049
  themeMode,
695
1050
  density
696
1051
  );
697
- return /* @__PURE__ */ jsx13(ThemeContext.Provider, { value: { themeMode, density, theme }, children: themedChildren });
1052
+ return /* @__PURE__ */ jsx14(ThemeContext.Provider, { value: { themeMode, density, theme }, children: themedChildren });
698
1053
  };
699
1054
  ThemeProvider.displayName = "ThemeProvider";
700
1055
  export {
@@ -705,6 +1060,7 @@ export {
705
1060
  DensitySwitch,
706
1061
  Feature,
707
1062
  LoginPanel,
1063
+ SessionEditingForm,
708
1064
  Shell,
709
1065
  ShellContextProvider,
710
1066
  ThemeContext,