@seedtactics/insight-client 16.8.4-beta.1 → 16.8.4-beta.10

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.
@@ -48,7 +48,7 @@ import { useAtomValue } from "jotai";
48
48
  import { fmsInformation } from "../network/server-settings.js";
49
49
  import { basketDisplayName, loadStationDisplayName } from "../cell-status/station-cycles.js";
50
50
  import { filterRemoveAddQueue } from "./log-entry-queue-filter.js";
51
- import { basketContainerName } from "./log-entry-container-name.js";
51
+ import { basketContainerName, basketCycleDescription } from "./log-entry-container-name.js";
52
52
  const ColoredSpan = styled("span", { shouldForwardProp: (prop) => prop.toString()[0] !== "$" })(({ $type }) => {
53
53
  switch ($type) {
54
54
  case "machine":
@@ -191,14 +191,7 @@ function display(props, fmsInfo) {
191
191
  }
192
192
  case api.LogType.BasketCycle: {
193
193
  const basketName = basketDisplayName(fmsInfo.basketName);
194
- if (entry.startofcycle) {
195
- return (_jsxs("span", { children: [basketName, " ", entry.pal, " started cycle"] }));
196
- }
197
- else {
198
- return (_jsxs("span", { children: [basketName, " ", entry.pal, " completed cycle", entry.containerIds && entry.containerIds.length > 0
199
- ? ` from ${entry.containerIds.length} UUID fragment${entry.containerIds.length === 1 ? "" : "s"}`
200
- : ""] }));
201
- }
194
+ return _jsx("span", { children: basketCycleDescription(entry, basketName) });
202
195
  }
203
196
  case api.LogType.BasketInLocation: {
204
197
  const basketName = basketDisplayName(fmsInfo.basketName);
@@ -1,4 +1,6 @@
1
1
  import * as api from "../network/api.js";
2
2
  type ContainerNameEntry = Readonly<Pick<api.ILogEntry, "pal" | "containerId">>;
3
+ type BasketCycleEntry = ContainerNameEntry & Readonly<Pick<api.ILogEntry, "startofcycle" | "containerIds">>;
3
4
  export declare function basketContainerName(entry: ContainerNameEntry, basketName: string): string;
5
+ export declare function basketCycleDescription(entry: BasketCycleEntry, basketName: string): string;
4
6
  export {};
@@ -5,3 +5,13 @@ export function basketContainerName(entry, basketName) {
5
5
  return `${basketName} ${entry.pal}`;
6
6
  return basketName;
7
7
  }
8
+ export function basketCycleDescription(entry, basketName) {
9
+ const container = basketContainerName(entry, basketName);
10
+ if (entry.startofcycle)
11
+ return `${container} started cycle`;
12
+ const fragmentCount = entry.containerIds?.length ?? 0;
13
+ const fragments = fragmentCount > 0
14
+ ? ` from ${fragmentCount} UUID fragment${fragmentCount === 1 ? "" : "s"}`
15
+ : "";
16
+ return `${container} completed cycle${fragments}`;
17
+ }
@@ -249,7 +249,7 @@ export function ProgramSummaryTable() {
249
249
  function ProgramContentCode() {
250
250
  const ct = useAtomValue(programContent);
251
251
  const [highlighted, setHighlighted] = useState(null);
252
- const worker = useMemo(() => new Worker(new URL("./ProgramHighlight.ts", import.meta.url), { type: "module" }), []);
252
+ const worker = useMemo(() => new Worker(new URL("./ProgramHighlight.js", import.meta.url), { type: "module" }), []);
253
253
  useEffect(() => {
254
254
  let set = (h) => setHighlighted(h);
255
255
  const handleMessage = (event) => set(event.data);
@@ -28,81 +28,86 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
28
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
29
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
30
  */
31
- import { useEffect, useMemo, useState } from "react";
31
+ import { useMemo, useState } from "react";
32
32
  import { Alert, Box, Button, Card, CardContent, Stack, Typography } from "@mui/material";
33
33
  import { LazySeq } from "@seedtactics/immutable-collections";
34
34
  import * as api from "../../network/api.js";
35
35
  import { InProcMaterial } from "./Material.js";
36
36
  import { MoveMaterialArrowNode } from "./MoveMaterialArrows.js";
37
37
  import { MoveMaterialNodeKindType } from "../../data/move-arrows.js";
38
- function isNonNegativeInteger(value) {
39
- return Number.isInteger(value) && (value ?? -1) >= 0;
38
+ function isPositiveInteger(value) {
39
+ return Number.isInteger(value) && (value ?? 0) > 0;
40
40
  }
41
41
  function isNonBlank(value) {
42
42
  return typeof value === "string" && value.trim() !== "";
43
43
  }
44
- function actionPhase(mat, basketId) {
44
+ function basketAction(mat, basketId) {
45
45
  if (mat.location.type === api.LocType.InBasket &&
46
46
  mat.location.basketId === basketId &&
47
47
  (mat.action.type === api.ActionType.UnloadToInProcess ||
48
48
  mat.action.type === api.ActionType.UnloadToCompletedMaterial)) {
49
- if (!isNonNegativeInteger(mat.location.basketSlot)) {
50
- return "invalid";
49
+ if (!isPositiveInteger(mat.location.basketSlot)) {
50
+ return { type: "invalid" };
51
51
  }
52
- return "unload";
52
+ return { type: "valid", workId: mat.action.workId };
53
53
  }
54
54
  if (mat.action.type === api.ActionType.LoadingToBasket) {
55
- if (!isNonNegativeInteger(mat.action.loadToBasketId))
56
- return "invalid";
55
+ if (!isPositiveInteger(mat.action.loadToBasketId))
56
+ return { type: "invalid" };
57
57
  if (mat.action.loadToBasketId !== basketId)
58
58
  return undefined;
59
- if (!isNonNegativeInteger(mat.action.loadToBasketSlot) ||
60
- (mat.location.type !== api.LocType.Free &&
59
+ const reloadsSameSlot = mat.location.type === api.LocType.InBasket &&
60
+ mat.location.basketId === basketId &&
61
+ mat.location.basketSlot === mat.action.loadToBasketSlot;
62
+ if (!isPositiveInteger(mat.action.loadToBasketSlot) ||
63
+ (!reloadsSameSlot &&
64
+ mat.location.type !== api.LocType.Free &&
61
65
  (mat.location.type !== api.LocType.InQueue || !isNonBlank(mat.location.currentQueue)))) {
62
- return "invalid";
66
+ return { type: "invalid" };
63
67
  }
64
- return "load";
68
+ return { type: "valid", workId: mat.action.workId };
65
69
  }
66
70
  return undefined;
67
71
  }
68
72
  function confirmableWork(material, basketId) {
69
73
  const classifiedActions = LazySeq.of(material)
70
- .collect((mat) => {
71
- const phase = actionPhase(mat, basketId);
72
- return phase === undefined ? undefined : { phase, workId: mat.action.workId };
73
- })
74
+ .collect((mat) => basketAction(mat, basketId))
74
75
  .toRArray();
75
- if (classifiedActions.some(({ phase }) => phase === "invalid")) {
76
+ if (classifiedActions.some((action) => action.type === "invalid")) {
76
77
  return { type: "inconsistent" };
77
78
  }
78
- const phaseActions = classifiedActions.filter((action) => action.phase !== "invalid");
79
- if (phaseActions.length === 0)
79
+ const validActions = classifiedActions.filter((action) => action.type === "valid");
80
+ if (validActions.length === 0)
80
81
  return { type: "none" };
81
- const tagged = phaseActions.filter((action) => isNonBlank(action.workId));
82
+ const tagged = validActions.filter((action) => isNonBlank(action.workId));
82
83
  const workIds = LazySeq.of(tagged).toHashSet(({ workId }) => workId);
83
- const phases = LazySeq.of(tagged).toHashSet(({ phase }) => phase);
84
- if (tagged.length !== phaseActions.length || workIds.size !== 1 || phases.size !== 1) {
84
+ if (tagged.length !== validActions.length || workIds.size !== 1) {
85
85
  return { type: "inconsistent" };
86
86
  }
87
87
  return {
88
88
  type: "confirmable",
89
- work: { workId: tagged[0].workId, phase: tagged[0].phase },
89
+ work: { workId: tagged[0].workId },
90
90
  };
91
91
  }
92
- function loadingSource(mat) {
92
+ function loadingInstruction(mat) {
93
+ if (mat.location.type === api.LocType.InBasket &&
94
+ mat.location.basketId === mat.action.loadToBasketId &&
95
+ mat.location.basketSlot === mat.action.loadToBasketSlot) {
96
+ return "Unload, re-plate, and reload this slot";
97
+ }
93
98
  if (mat.location.type === api.LocType.InQueue && mat.location.currentQueue) {
94
- return mat.location.currentQueue;
99
+ return `Load from ${mat.location.currentQueue}`;
95
100
  }
96
- return "raw material";
101
+ return "Load from raw material";
97
102
  }
98
103
  function SlotMaterial({ material, fsize, }) {
99
104
  return material.map((mat) => (_jsx(MoveMaterialArrowNode, { kind: { type: MoveMaterialNodeKindType.Material, material: mat }, children: _jsx(InProcMaterial, { mat: mat, fsize: fsize }) }, mat.materialID)));
100
105
  }
101
106
  export function BasketLoadStationWorkflow({ stationNumber, basket, material, fsize, submitCommand, }) {
102
- const [submission, setSubmission] = useState("idle");
107
+ const [submission, setSubmission] = useState();
103
108
  const workState = useMemo(() => confirmableWork(material, basket.basketId), [basket.basketId, material]);
104
109
  const work = workState.type === "confirmable" ? workState.work : undefined;
105
- useEffect(() => setSubmission("idle"), [work?.workId]);
110
+ const submissionState = submission !== undefined && submission.workId === work?.workId ? submission.state : "idle";
106
111
  const materialBySlot = useMemo(() => LazySeq.of(material)
107
112
  .filter((mat) => mat.location.type === api.LocType.InBasket &&
108
113
  mat.location.basketId === basket.basketId &&
@@ -124,16 +129,19 @@ export function BasketLoadStationWorkflow({ stationNumber, basket, material, fsi
124
129
  async function submit() {
125
130
  if (work === undefined || submitCommand === undefined)
126
131
  return;
127
- setSubmission("submitting");
132
+ const submittedWorkId = work.workId;
133
+ setSubmission({ workId: submittedWorkId, state: "submitting" });
128
134
  try {
129
- setSubmission(await submitCommand(stationNumber, { workId: work.workId }));
135
+ const state = await submitCommand(stationNumber, { workId: submittedWorkId });
136
+ setSubmission((current) => current?.workId === submittedWorkId ? { workId: submittedWorkId, state } : current);
130
137
  }
131
138
  catch {
132
- setSubmission("error");
139
+ setSubmission((current) => current?.workId === submittedWorkId ? { workId: submittedWorkId, state: "error" } : current);
133
140
  }
134
141
  }
135
- const submissionDisabled = submission === "submitting" || submission === "accepted" || submission === "conflict";
136
- const buttonLabel = work?.phase === "unload" ? "Unload Complete" : "Load Complete";
142
+ const submissionDisabled = submissionState === "submitting" ||
143
+ submissionState === "accepted" ||
144
+ submissionState === "conflict";
137
145
  return (_jsxs(Stack, { spacing: 2, sx: { mt: 2 }, children: [_jsx(Typography, { variant: "h6", children: "Basket slots" }), _jsx(Box, { sx: {
138
146
  display: "grid",
139
147
  gridTemplateColumns: "repeat(auto-fit, minmax(15em, 1fr))",
@@ -142,6 +150,6 @@ export function BasketLoadStationWorkflow({ stationNumber, basket, material, fsi
142
150
  const slotMaterial = materialBySlot.get(slot) ?? [];
143
151
  const loads = loadsBySlot.get(slot) ?? [];
144
152
  const isUnknown = basket.unknownSlots?.includes(slot) === true;
145
- return (_jsx(Card, { "data-testid": `basket-load-station-slot-${slot}`, variant: "outlined", children: _jsxs(CardContent, { children: [_jsxs(Typography, { variant: "h6", children: ["Slot ", slot + 1] }), slotMaterial.length > 0 ? (_jsx(SlotMaterial, { material: slotMaterial, fsize: fsize })) : (_jsx(Typography, { color: isUnknown ? "warning.main" : "text.secondary", children: isUnknown ? "Unknown" : "Empty" })), loads.map((mat, index) => (_jsxs(Box, { sx: { mt: 1 }, children: [_jsxs(Typography, { children: ["Load from ", loadingSource(mat)] }), _jsxs(Typography, { variant: "body2", color: "text.secondary", children: [mat.partName, " \u00B7 Process ", mat.action.processAfterLoad ?? mat.process] })] }, `${mat.jobUnique}-${mat.process}-${index}`)))] }) }, slot));
146
- }) }), work && submitCommand ? (_jsx(Box, { sx: { display: "flex", justifyContent: "flex-end" }, children: _jsx(Button, { variant: "contained", disabled: submissionDisabled, onClick: () => void submit(), children: buttonLabel }) })) : work ? (_jsx(Alert, { severity: "warning", children: "Basket work confirmation is not configured." })) : workState.type === "inconsistent" ? (_jsx(Alert, { severity: "warning", children: "Basket work is inconsistent. Wait for refreshed material actions before confirming." })) : null, submission === "accepted" ? (_jsx(Alert, { severity: "success", children: "Confirmation accepted. Waiting for refreshed work." })) : submission === "conflict" ? (_jsx(Alert, { severity: "warning", children: "Basket work changed before confirmation. Review the refreshed slots and try again." })) : submission === "error" ? (_jsx(Alert, { severity: "error", children: "Unable to confirm basket work. No work was assumed complete." })) : null] }));
153
+ return (_jsx(Card, { "data-testid": `basket-load-station-slot-${slot}`, variant: "outlined", children: _jsxs(CardContent, { children: [_jsxs(Typography, { variant: "h6", children: ["Slot ", slot] }), slotMaterial.length > 0 ? (_jsx(SlotMaterial, { material: slotMaterial, fsize: fsize })) : (_jsx(Typography, { color: isUnknown ? "warning.main" : "text.secondary", children: isUnknown ? "Unknown" : "Empty" })), loads.map((mat, index) => (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { children: loadingInstruction(mat) }), _jsxs(Typography, { variant: "body2", color: "text.secondary", children: [mat.partName, " \u00B7 Process ", mat.action.processAfterLoad ?? mat.process] })] }, `${mat.jobUnique}-${mat.process}-${index}`)))] }) }, slot));
154
+ }) }), work && submitCommand ? (_jsx(Box, { sx: { display: "flex", justifyContent: "flex-end" }, children: _jsx(Button, { variant: "contained", disabled: submissionDisabled, onClick: () => void submit(), children: "Confirm" }) })) : work ? (_jsx(Alert, { severity: "warning", children: "Basket work confirmation is not configured." })) : workState.type === "inconsistent" ? (_jsx(Alert, { severity: "warning", children: "Basket work is inconsistent. Wait for refreshed material actions before confirming." })) : null, submissionState === "accepted" ? (_jsx(Alert, { severity: "success", children: "Confirmation accepted. Waiting for refreshed work." })) : submissionState === "conflict" ? (_jsx(Alert, { severity: "warning", children: "Basket work changed before confirmation. Review the refreshed slots and try again." })) : submissionState === "error" ? (_jsx(Alert, { severity: "error", children: "Unable to confirm basket work. No work was assumed complete." })) : null] }));
147
155
  }
@@ -235,14 +235,12 @@ export function MaterialAction({ mat, displayActionForSinglePallet, fsize, }) {
235
235
  }
236
236
  }
237
237
  case api.ActionType.LoadingToBasket:
238
- return (_jsxs(MatCardDetail, { fsize: fsize, children: ["Load into ", basketName, " ", mat.action.loadToBasketId ?? "", mat.action.loadToBasketSlot !== undefined
239
- ? ` slot ${mat.action.loadToBasketSlot + 1}`
240
- : ""] }));
238
+ return (_jsxs(MatCardDetail, { fsize: fsize, children: ["Load into ", basketName, " ", mat.action.loadToBasketId ?? "", mat.action.loadToBasketSlot !== undefined ? ` slot ${mat.action.loadToBasketSlot}` : ""] }));
241
239
  case api.ActionType.UnloadToInProcess:
242
240
  case api.ActionType.UnloadToCompletedMaterial:
243
241
  if (mat.action.unloadToBasketId) {
244
242
  return (_jsxs(MatCardDetail, { fsize: fsize, children: ["Unload to ", basketName, " ", mat.action.unloadToBasketId, mat.action.unloadToBasketSlot !== undefined
245
- ? ` slot ${mat.action.unloadToBasketSlot + 1}`
243
+ ? ` slot ${mat.action.unloadToBasketSlot}`
246
244
  : ""] }));
247
245
  }
248
246
  else if (mat.action.unloadIntoQueue) {
@@ -446,7 +446,7 @@ function PalletFaces({ maxNumFaces, mats: allMats, loadingOntoPallet, noFilter,
446
446
  }
447
447
  }
448
448
  function BasketContents({ mats }) {
449
- const byPosition = LazySeq.of(mats).orderedGroupBy((m) => (m.location.basketSlot ?? 0) + 1);
449
+ const byPosition = LazySeq.of(mats).orderedGroupBy((m) => m.location.basketSlot ?? 0);
450
450
  return (_jsx(Box, { sx: {
451
451
  display: "flex",
452
452
  flexWrap: "wrap",
@@ -0,0 +1,11 @@
1
+ import type { AppProps } from "./components/App.js";
2
+ /**
3
+ * Starts one complete FMS Insight application with optional statically composed custom pages.
4
+ */
5
+ export declare function bootstrapInsight(appProps?: AppProps, rootElement?: HTMLElement): Promise<void>;
6
+ export { customState } from "./cell-status/custom-state.js";
7
+ export type { AppProps };
8
+ export { defaultChooseModes } from "./components/ChooseMode.js";
9
+ export type { ChooseModeItem } from "./components/ChooseMode.js";
10
+ export { RouteLocation } from "./components/routes.js";
11
+ export { authenticatedFetch } from "./network/backend.js";
@@ -0,0 +1,21 @@
1
+ import { createStore } from "jotai";
2
+ import { registerNetworkBackend } from "./network/backend.js";
3
+ import { fmsInformation, loadInfo } from "./network/server-settings.js";
4
+ import { render } from "./renderer.js";
5
+ /**
6
+ * Starts one complete FMS Insight application with optional statically composed custom pages.
7
+ */
8
+ export async function bootstrapInsight(appProps = {}, rootElement) {
9
+ const root = rootElement ?? document.getElementById("root");
10
+ if (!root) {
11
+ throw new Error("Unable to start FMS Insight: no root element was provided or found");
12
+ }
13
+ registerNetworkBackend();
14
+ const store = createStore();
15
+ store.set(fmsInformation, await loadInfo());
16
+ render(appProps, root, store);
17
+ }
18
+ export { customState } from "./cell-status/custom-state.js";
19
+ export { defaultChooseModes } from "./components/ChooseMode.js";
20
+ export { RouteLocation } from "./components/routes.js";
21
+ export { authenticatedFetch } from "./network/backend.js";
package/dist/index.html CHANGED
@@ -3,7 +3,6 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
6
- <link rel="manifest" href="/assets/manifest-BzJli0lx.webmanifest" />
7
6
  <link rel="shortcut icon" href="/assets/favicon-gWFxiJK2.ico" />
8
7
  <title>SeedTactic FMS Insight</title>
9
8
  <style type="text/css">
@@ -44,7 +43,7 @@
44
43
  }
45
44
  }
46
45
  </style>
47
- <script type="module" crossorigin src="/assets/index-Y5yNHUvc.js"></script>
46
+ <script type="module" crossorigin src="/assets/index-BB3Eqktr.js"></script>
48
47
  <link rel="stylesheet" crossorigin href="/assets/index-DXfAwuj-.css">
49
48
  </head>
50
49
  <body>
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30
30
  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
31
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
32
  */
33
- import { bootstrapInsight } from "./bootstrap.js";
33
+ import { bootstrapInsight } from "./extension.js";
34
34
  import { ApiException } from "./network/api.js";
35
35
  async function main() {
36
36
  const developmentProps = import.meta.env.DEV
@@ -64,7 +64,8 @@ export function setUserToken(u) {
64
64
  }
65
65
  /** Performs a request using the authenticated Insight user's bearer token, when present. */
66
66
  export function authenticatedFetch(url, init) {
67
- const headers = new Headers(init?.headers);
67
+ const headers = new Headers(url instanceof Request ? url.headers : undefined);
68
+ new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
68
69
  if (userAccessToken) {
69
70
  headers.set("Authorization", "Bearer " + userAccessToken);
70
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seedtactics/insight-client",
3
- "version": "16.8.4-beta.1",
3
+ "version": "16.8.4-beta.10",
4
4
  "license": "BSD-3-Clause",
5
5
  "repository": {
6
6
  "url": "https://github.com/SeedTactics/fms-insight"
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "type": "module",
13
13
  "exports": {
14
- "./bootstrap": "./dist/bootstrap.js",
14
+ "./extension": "./dist/extension.js",
15
15
  "./cell-status/*": "./dist/cell-status/*.js",
16
16
  "./components/*": "./dist/components/*.js",
17
17
  "./components/analysis/*": "./dist/components/analysis/*.js",
@@ -46,9 +46,9 @@
46
46
  "jotai-family": "^1.0.2",
47
47
  "jsbarcode": "^3.12.3",
48
48
  "oidc-client-ts": "^3.5.0",
49
- "react": "^19.2.7",
49
+ "react": "^19.2.8",
50
50
  "react-calendar": "^6.0.1",
51
- "react-dom": "^19.2.7",
51
+ "react-dom": "^19.2.8",
52
52
  "react-error-boundary": "^6.1.2",
53
53
  "react-resize-detector": "^12.3.0",
54
54
  "react-timeago": "^8.3.0",
@@ -61,16 +61,17 @@
61
61
  "@types/d3-sankey": "^0.12.5",
62
62
  "@types/d3-scale": "^4.0.9",
63
63
  "@types/d3-shape": "^3.1.8",
64
- "@types/node": "^26.1.1",
64
+ "@types/node": "^26.1.2",
65
65
  "@types/react": "^19.2.17",
66
66
  "@types/react-dom": "^19.2.3",
67
- "@vitejs/plugin-react": "^6.0.3",
67
+ "@vitejs/plugin-react": "^6.0.4",
68
68
  "@vitest/browser-playwright": "^4.1.10",
69
- "jsdom": "^29.1.1",
69
+ "jsdom": "^30.0.1",
70
70
  "nswag": "^14.7.1",
71
- "oxfmt": "^0.59.0",
72
- "oxlint": "^1.74.0",
73
- "oxlint-tsgolint": "^0.25.0",
71
+ "oxfmt": "^0.61.0",
72
+ "oxlint": "^1.76.0",
73
+ "oxlint-tsgolint": "^7.0.2001",
74
+ "playwright": "^1.62.0",
74
75
  "typescript": "^7.0.2",
75
76
  "vite": "^8.1.5",
76
77
  "vitest": "^4.1.10",
@@ -79,7 +80,7 @@
79
80
  "scripts": {
80
81
  "type-check": "tsc --noEmit --skipLibCheck",
81
82
  "lint": "oxlint --type-aware",
82
- "build": "tsc --noEmit --skipLibCheck && vite build",
83
+ "build": "vite build",
83
84
  "build:package": "tsc --project tsconfig.publish.json && cp -Tr ../../docs ./docs",
84
85
  "start": "vite",
85
86
  "test:unit": "vitest run --project unit",