@lotics/cli 0.53.0 → 0.54.2
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/dev/server.js +4 -1
- package/dist/dev/wrapper_page.d.ts +3 -0
- package/dist/dev/wrapper_page.js +53 -0
- package/dist/src/cli.js +81 -5
- package/dist/starter_template.d.ts +5 -3
- package/dist/starter_template.js +33 -6
- package/package.json +1 -1
package/dist/dev/server.js
CHANGED
|
@@ -86,7 +86,10 @@ export async function startDevServer(args) {
|
|
|
86
86
|
// ── HTTP server ────────────────────────────────────────────────────────
|
|
87
87
|
const server = http.createServer(async (req, res) => {
|
|
88
88
|
const url = req.url ?? "/";
|
|
89
|
-
|
|
89
|
+
// Route by pathname — `useUrlState` writes a query string onto the wrapper
|
|
90
|
+
// URL, so a refresh / shared link requests `/?…`; the page must still serve.
|
|
91
|
+
const pathname = url.split("?")[0];
|
|
92
|
+
if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
|
|
90
93
|
res.writeHead(200, {
|
|
91
94
|
"Content-Type": "text/html; charset=utf-8",
|
|
92
95
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
* wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
|
|
25
25
|
* streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
|
|
26
26
|
* the iframe aborts with { id, type: "abort" }.
|
|
27
|
+
* url-state (useUrlState): the iframe reads/writes the wrapper's address bar
|
|
28
|
+
* via the urlState.get/set ops; the wrapper pushes { type: "url-state",
|
|
29
|
+
* params } (no id) on back/forward.
|
|
27
30
|
*/
|
|
28
31
|
export interface WrapperPageArgs {
|
|
29
32
|
app_name: string;
|
package/dist/dev/wrapper_page.js
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
* wrapper → iframe: { id, type: "result", data } | { id, type: "error", message }
|
|
25
25
|
* streaming (op "agentRun"): { id, type: "stream-chunk", chunk } * → { id, type: "stream-end" };
|
|
26
26
|
* the iframe aborts with { id, type: "abort" }.
|
|
27
|
+
* url-state (useUrlState): the iframe reads/writes the wrapper's address bar
|
|
28
|
+
* via the urlState.get/set ops; the wrapper pushes { type: "url-state",
|
|
29
|
+
* params } (no id) on back/forward.
|
|
27
30
|
*/
|
|
28
31
|
export function buildWrapperPage(args) {
|
|
29
32
|
const { app_name, app_id, workspace_id, vite_url, api_url } = args;
|
|
@@ -165,6 +168,42 @@ export function buildWrapperPage(args) {
|
|
|
165
168
|
return undefined;
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
// urlState.get/set — useUrlState keeps app view-state (filters, search) in
|
|
172
|
+
// THIS wrapper page's address bar so it survives refresh and is shareable,
|
|
173
|
+
// mirroring the production host. Handled locally (the URL is the store; no
|
|
174
|
+
// API hop). The wrapper page is its own top-level page, so its query string
|
|
175
|
+
// is entirely the app's. Repeated keys become arrays. Parse/merge MUST
|
|
176
|
+
// match @lotics/app-sdk parseSearch/serializeMerge and the production host
|
|
177
|
+
// (inline JS here — no module system to share the trivial copy).
|
|
178
|
+
function readUrlParams() {
|
|
179
|
+
const sp = new URLSearchParams(window.location.search);
|
|
180
|
+
const out = {};
|
|
181
|
+
sp.forEach(function (_value, key) {
|
|
182
|
+
if (key in out) return;
|
|
183
|
+
const all = sp.getAll(key);
|
|
184
|
+
out[key] = all.length > 1 ? all : all[0];
|
|
185
|
+
});
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function handleUrlStateSet(payload) {
|
|
189
|
+
const params = (payload && payload.params) || {};
|
|
190
|
+
const sp = new URLSearchParams(window.location.search);
|
|
191
|
+
Object.keys(params).forEach(function (key) {
|
|
192
|
+
sp.delete(key);
|
|
193
|
+
const value = params[key];
|
|
194
|
+
if (value === undefined || value === null) return;
|
|
195
|
+
if (Array.isArray(value)) value.forEach(function (item) { sp.append(key, item); });
|
|
196
|
+
else sp.append(key, value);
|
|
197
|
+
});
|
|
198
|
+
const qs = sp.toString();
|
|
199
|
+
const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
|
|
200
|
+
// pushState/replaceState don't fire popstate, so no echo — the app
|
|
201
|
+
// updated itself optimistically on write.
|
|
202
|
+
if (payload && payload.push) window.history.pushState(null, "", url);
|
|
203
|
+
else window.history.replaceState(null, "", url);
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
168
207
|
// Streaming agent runs (op "agentRun"): the response is a stream, so it
|
|
169
208
|
// can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
|
|
170
209
|
// body, and forward chunks to the iframe as the SDK's bridged streaming
|
|
@@ -226,6 +265,10 @@ export function buildWrapperPage(args) {
|
|
|
226
265
|
? await handleUpload(msg.payload)
|
|
227
266
|
: msg.op === "openExternal"
|
|
228
267
|
? handleOpenExternal(msg.payload)
|
|
268
|
+
: msg.op === "urlState.get"
|
|
269
|
+
? readUrlParams()
|
|
270
|
+
: msg.op === "urlState.set"
|
|
271
|
+
? handleUrlStateSet(msg.payload)
|
|
229
272
|
: await rpc(msg.op, msg.payload);
|
|
230
273
|
const ms = Math.round(performance.now() - startedAt);
|
|
231
274
|
console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
|
|
@@ -242,6 +285,16 @@ export function buildWrapperPage(args) {
|
|
|
242
285
|
);
|
|
243
286
|
}
|
|
244
287
|
});
|
|
288
|
+
|
|
289
|
+
// Push the URL back into the app on back/forward so useUrlState
|
|
290
|
+
// re-hydrates (the app's own writes use replace/pushState — no popstate —
|
|
291
|
+
// so there's no echo). Matches the production host.
|
|
292
|
+
window.addEventListener("popstate", function () {
|
|
293
|
+
iframe.contentWindow.postMessage(
|
|
294
|
+
{ type: "url-state", params: readUrlParams() },
|
|
295
|
+
VITE_ORIGIN
|
|
296
|
+
);
|
|
297
|
+
});
|
|
245
298
|
})();
|
|
246
299
|
</script>
|
|
247
300
|
</body>
|
package/dist/src/cli.js
CHANGED
|
@@ -30241,7 +30241,7 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
30241
30241
|
import { tmpdir } from "node:os";
|
|
30242
30242
|
|
|
30243
30243
|
// src/starter_template.ts
|
|
30244
|
-
var STARTER_FALLBACK_UI_VERSION = "
|
|
30244
|
+
var STARTER_FALLBACK_UI_VERSION = "6.1.0";
|
|
30245
30245
|
var STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
30246
30246
|
var STARTER_REACT_NATIVE_VERSION = "0.85.3";
|
|
30247
30247
|
function buildStarterTemplate(args) {
|
|
@@ -30268,11 +30268,18 @@ function buildStarterTemplate(args) {
|
|
|
30268
30268
|
},
|
|
30269
30269
|
dependencies: {
|
|
30270
30270
|
"@lotics/app-sdk": sdkVersion,
|
|
30271
|
+
// Document preview engines for @lotics/ui FilePreview/FileGalleryModal
|
|
30272
|
+
// (Word, Excel/CSV). Lazy-loaded — an app that never previews them
|
|
30273
|
+
// pays no bundle cost; only node_modules size grows.
|
|
30274
|
+
"@lotics/docx": "^0.1.0",
|
|
30271
30275
|
"@lotics/ui": uiVersion,
|
|
30276
|
+
"@lotics/xlsx": "^0.1.0",
|
|
30272
30277
|
"@react-native-picker/picker": "^2.7.0",
|
|
30273
30278
|
"expo-image": "~3.0.9",
|
|
30274
30279
|
"lucide-react": "^0.562.0",
|
|
30275
30280
|
"lucide-react-native": "^0.562.0",
|
|
30281
|
+
// PDF preview engine for FilePreview/FileGalleryModal — also lazy-loaded.
|
|
30282
|
+
"pdfjs-dist": "^6.0.0",
|
|
30276
30283
|
react: "^19.0.0",
|
|
30277
30284
|
"react-dom": "^19.0.0",
|
|
30278
30285
|
"react-native": STARTER_REACT_NATIVE_VERSION,
|
|
@@ -30402,8 +30409,18 @@ export default defineConfig({
|
|
|
30402
30409
|
// otherwise serves them without a synthesized default export ("does not
|
|
30403
30410
|
// provide an export named 'default'"), blanking the iframe. (Production/rollup
|
|
30404
30411
|
// resolves the interop already, so this is dev-only.)
|
|
30412
|
+
//
|
|
30413
|
+
// \`react-markdown\` (@lotics/ui's Markdown renderer, reached by AgentRun and
|
|
30414
|
+
// any markdown surface) pulls a transitive CJS dep, \`style-to-js\`, whose
|
|
30415
|
+
// default export the dev optimizer won't synthesize unbundled ("does not
|
|
30416
|
+
// provide an export named 'default'") \u2014 blanking the iframe the moment a
|
|
30417
|
+
// markdown component mounts. Pre-bundling react-markdown folds the whole
|
|
30418
|
+
// subtree (incl. style-to-js) into one interop'd chunk; remark-gfm (imported
|
|
30419
|
+
// alongside it) is pre-bundled for the same consistency. Both are regular
|
|
30420
|
+
// deps of @lotics/ui (hoisted), so they need no package.json entry here.
|
|
30405
30421
|
include: [
|
|
30406
30422
|
"react-native-svg",
|
|
30423
|
+
"react-markdown", "remark-gfm",
|
|
30407
30424
|
"react-native-web", "@react-native/normalize-colors",
|
|
30408
30425
|
"inline-style-prefixer/lib/createPrefixer",
|
|
30409
30426
|
"inline-style-prefixer/lib/plugins/crossFade",
|
|
@@ -30526,15 +30543,23 @@ export default defineConfig({
|
|
|
30526
30543
|
content: `import "@lotics/ui/index.css";
|
|
30527
30544
|
import "@lotics/ui/fonts.css";
|
|
30528
30545
|
import { PortalHost } from "@lotics/ui/portal";
|
|
30546
|
+
import { LoticsLocaleProvider, vi } from "@lotics/ui/locale";
|
|
30529
30547
|
import { mount } from "@lotics/app-sdk";
|
|
30530
30548
|
import App from "./App";
|
|
30531
30549
|
|
|
30532
30550
|
// PortalHost is the render target for @lotics/ui overlays (Popover, Tooltip,
|
|
30533
30551
|
// Dialog). Without it, Portal renders nothing \u2014 keep it wrapping the app.
|
|
30552
|
+
//
|
|
30553
|
+
// LoticsLocaleProvider supplies the kit's own strings (Pagination, sort
|
|
30554
|
+
// headers, select-all, the Drawer's record nav\u2026) in one language \u2014 Vietnamese
|
|
30555
|
+
// by default, since Lotics apps are Vietnamese. Swap to \`en\` (or your own
|
|
30556
|
+
// LoticsLocale pack) for another language; remove it to fall back to English.
|
|
30534
30557
|
mount(
|
|
30535
|
-
<
|
|
30536
|
-
<
|
|
30537
|
-
|
|
30558
|
+
<LoticsLocaleProvider locale={vi}>
|
|
30559
|
+
<PortalHost>
|
|
30560
|
+
<App />
|
|
30561
|
+
</PortalHost>
|
|
30562
|
+
</LoticsLocaleProvider>,
|
|
30538
30563
|
);
|
|
30539
30564
|
`
|
|
30540
30565
|
},
|
|
@@ -31053,6 +31078,42 @@ function buildWrapperPage(args) {
|
|
|
31053
31078
|
return undefined;
|
|
31054
31079
|
}
|
|
31055
31080
|
|
|
31081
|
+
// urlState.get/set \u2014 useUrlState keeps app view-state (filters, search) in
|
|
31082
|
+
// THIS wrapper page's address bar so it survives refresh and is shareable,
|
|
31083
|
+
// mirroring the production host. Handled locally (the URL is the store; no
|
|
31084
|
+
// API hop). The wrapper page is its own top-level page, so its query string
|
|
31085
|
+
// is entirely the app's. Repeated keys become arrays. Parse/merge MUST
|
|
31086
|
+
// match @lotics/app-sdk parseSearch/serializeMerge and the production host
|
|
31087
|
+
// (inline JS here \u2014 no module system to share the trivial copy).
|
|
31088
|
+
function readUrlParams() {
|
|
31089
|
+
const sp = new URLSearchParams(window.location.search);
|
|
31090
|
+
const out = {};
|
|
31091
|
+
sp.forEach(function (_value, key) {
|
|
31092
|
+
if (key in out) return;
|
|
31093
|
+
const all = sp.getAll(key);
|
|
31094
|
+
out[key] = all.length > 1 ? all : all[0];
|
|
31095
|
+
});
|
|
31096
|
+
return out;
|
|
31097
|
+
}
|
|
31098
|
+
function handleUrlStateSet(payload) {
|
|
31099
|
+
const params = (payload && payload.params) || {};
|
|
31100
|
+
const sp = new URLSearchParams(window.location.search);
|
|
31101
|
+
Object.keys(params).forEach(function (key) {
|
|
31102
|
+
sp.delete(key);
|
|
31103
|
+
const value = params[key];
|
|
31104
|
+
if (value === undefined || value === null) return;
|
|
31105
|
+
if (Array.isArray(value)) value.forEach(function (item) { sp.append(key, item); });
|
|
31106
|
+
else sp.append(key, value);
|
|
31107
|
+
});
|
|
31108
|
+
const qs = sp.toString();
|
|
31109
|
+
const url = window.location.pathname + (qs ? "?" + qs : "") + window.location.hash;
|
|
31110
|
+
// pushState/replaceState don't fire popstate, so no echo \u2014 the app
|
|
31111
|
+
// updated itself optimistically on write.
|
|
31112
|
+
if (payload && payload.push) window.history.pushState(null, "", url);
|
|
31113
|
+
else window.history.replaceState(null, "", url);
|
|
31114
|
+
return undefined;
|
|
31115
|
+
}
|
|
31116
|
+
|
|
31056
31117
|
// Streaming agent runs (op "agentRun"): the response is a stream, so it
|
|
31057
31118
|
// can't use rpc()'s single JSON round-trip. POST /_agent_run, read the SSE
|
|
31058
31119
|
// body, and forward chunks to the iframe as the SDK's bridged streaming
|
|
@@ -31114,6 +31175,10 @@ function buildWrapperPage(args) {
|
|
|
31114
31175
|
? await handleUpload(msg.payload)
|
|
31115
31176
|
: msg.op === "openExternal"
|
|
31116
31177
|
? handleOpenExternal(msg.payload)
|
|
31178
|
+
: msg.op === "urlState.get"
|
|
31179
|
+
? readUrlParams()
|
|
31180
|
+
: msg.op === "urlState.set"
|
|
31181
|
+
? handleUrlStateSet(msg.payload)
|
|
31117
31182
|
: await rpc(msg.op, msg.payload);
|
|
31118
31183
|
const ms = Math.round(performance.now() - startedAt);
|
|
31119
31184
|
console.debug("[lotics-dev] " + msg.op + " " + ms + "ms", data);
|
|
@@ -31130,6 +31195,16 @@ function buildWrapperPage(args) {
|
|
|
31130
31195
|
);
|
|
31131
31196
|
}
|
|
31132
31197
|
});
|
|
31198
|
+
|
|
31199
|
+
// Push the URL back into the app on back/forward so useUrlState
|
|
31200
|
+
// re-hydrates (the app's own writes use replace/pushState \u2014 no popstate \u2014
|
|
31201
|
+
// so there's no echo). Matches the production host.
|
|
31202
|
+
window.addEventListener("popstate", function () {
|
|
31203
|
+
iframe.contentWindow.postMessage(
|
|
31204
|
+
{ type: "url-state", params: readUrlParams() },
|
|
31205
|
+
VITE_ORIGIN
|
|
31206
|
+
);
|
|
31207
|
+
});
|
|
31133
31208
|
})();
|
|
31134
31209
|
</script>
|
|
31135
31210
|
</body>
|
|
@@ -31188,7 +31263,8 @@ async function startDevServer(args) {
|
|
|
31188
31263
|
process.once("exit", killViteOnExit);
|
|
31189
31264
|
const server = http.createServer(async (req, res) => {
|
|
31190
31265
|
const url = req.url ?? "/";
|
|
31191
|
-
|
|
31266
|
+
const pathname = url.split("?")[0];
|
|
31267
|
+
if (req.method === "GET" && (pathname === "/" || pathname === "/index.html")) {
|
|
31192
31268
|
res.writeHead(200, {
|
|
31193
31269
|
"Content-Type": "text/html; charset=utf-8",
|
|
31194
31270
|
"Cache-Control": "no-cache, no-store, must-revalidate"
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
13
|
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
-
* `mount(<PortalHost><App/></PortalHost>)` —
|
|
15
|
-
*
|
|
14
|
+
* `mount(<LoticsLocaleProvider locale={vi}><PortalHost><App/></PortalHost></LoticsLocaleProvider>)` —
|
|
15
|
+
* LoticsLocaleProvider sets the kit's strings to Vietnamese (Lotics apps are
|
|
16
|
+
* Vietnamese; swap to `en` for English); PortalHost is the render target
|
|
17
|
+
* @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
|
|
16
18
|
* @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
17
19
|
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
18
20
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
@@ -39,7 +41,7 @@ export interface StarterFile {
|
|
|
39
41
|
* scaffolds resolve the live version via `fetchLatestNpmVersion` and only
|
|
40
42
|
* fall back here when the lookup fails.
|
|
41
43
|
*/
|
|
42
|
-
export declare const STARTER_FALLBACK_UI_VERSION = "
|
|
44
|
+
export declare const STARTER_FALLBACK_UI_VERSION = "6.1.0";
|
|
43
45
|
export declare const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
44
46
|
/**
|
|
45
47
|
* react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
|
package/dist/starter_template.js
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* Conventions (locked decisions):
|
|
12
12
|
* - Vite + React + TypeScript (strict mode).
|
|
13
13
|
* - Entry: src/App.tsx with `export default`. main.tsx wires
|
|
14
|
-
* `mount(<PortalHost><App/></PortalHost>)` —
|
|
15
|
-
*
|
|
14
|
+
* `mount(<LoticsLocaleProvider locale={vi}><PortalHost><App/></PortalHost></LoticsLocaleProvider>)` —
|
|
15
|
+
* LoticsLocaleProvider sets the kit's strings to Vietnamese (Lotics apps are
|
|
16
|
+
* Vietnamese; swap to `en` for English); PortalHost is the render target
|
|
17
|
+
* @lotics/ui overlays (Popover/Tooltip/Dialog) need — and imports
|
|
16
18
|
* @lotics/ui/index.css (base style reset) + @lotics/ui/fonts.css
|
|
17
19
|
* (path-independent Inter @font-face bundle — Text renders unstyled without it).
|
|
18
20
|
* - Vite default `base: "/"` so emitted asset URLs are absolute; the render
|
|
@@ -35,7 +37,7 @@
|
|
|
35
37
|
* scaffolds resolve the live version via `fetchLatestNpmVersion` and only
|
|
36
38
|
* fall back here when the lookup fails.
|
|
37
39
|
*/
|
|
38
|
-
export const STARTER_FALLBACK_UI_VERSION = "
|
|
40
|
+
export const STARTER_FALLBACK_UI_VERSION = "6.1.0";
|
|
39
41
|
export const STARTER_FALLBACK_SDK_VERSION = "0.11.0";
|
|
40
42
|
/**
|
|
41
43
|
* react-native pin for scaffolded apps. Matches the monorepo frontend's pin so
|
|
@@ -75,11 +77,18 @@ export function buildStarterTemplate(args) {
|
|
|
75
77
|
},
|
|
76
78
|
dependencies: {
|
|
77
79
|
"@lotics/app-sdk": sdkVersion,
|
|
80
|
+
// Document preview engines for @lotics/ui FilePreview/FileGalleryModal
|
|
81
|
+
// (Word, Excel/CSV). Lazy-loaded — an app that never previews them
|
|
82
|
+
// pays no bundle cost; only node_modules size grows.
|
|
83
|
+
"@lotics/docx": "^0.1.0",
|
|
78
84
|
"@lotics/ui": uiVersion,
|
|
85
|
+
"@lotics/xlsx": "^0.1.0",
|
|
79
86
|
"@react-native-picker/picker": "^2.7.0",
|
|
80
87
|
"expo-image": "~3.0.9",
|
|
81
88
|
"lucide-react": "^0.562.0",
|
|
82
89
|
"lucide-react-native": "^0.562.0",
|
|
90
|
+
// PDF preview engine for FilePreview/FileGalleryModal — also lazy-loaded.
|
|
91
|
+
"pdfjs-dist": "^6.0.0",
|
|
83
92
|
react: "^19.0.0",
|
|
84
93
|
"react-dom": "^19.0.0",
|
|
85
94
|
"react-native": STARTER_REACT_NATIVE_VERSION,
|
|
@@ -202,8 +211,18 @@ export default defineConfig({
|
|
|
202
211
|
// otherwise serves them without a synthesized default export ("does not
|
|
203
212
|
// provide an export named 'default'"), blanking the iframe. (Production/rollup
|
|
204
213
|
// resolves the interop already, so this is dev-only.)
|
|
214
|
+
//
|
|
215
|
+
// \`react-markdown\` (@lotics/ui's Markdown renderer, reached by AgentRun and
|
|
216
|
+
// any markdown surface) pulls a transitive CJS dep, \`style-to-js\`, whose
|
|
217
|
+
// default export the dev optimizer won't synthesize unbundled ("does not
|
|
218
|
+
// provide an export named 'default'") — blanking the iframe the moment a
|
|
219
|
+
// markdown component mounts. Pre-bundling react-markdown folds the whole
|
|
220
|
+
// subtree (incl. style-to-js) into one interop'd chunk; remark-gfm (imported
|
|
221
|
+
// alongside it) is pre-bundled for the same consistency. Both are regular
|
|
222
|
+
// deps of @lotics/ui (hoisted), so they need no package.json entry here.
|
|
205
223
|
include: [
|
|
206
224
|
"react-native-svg",
|
|
225
|
+
"react-markdown", "remark-gfm",
|
|
207
226
|
"react-native-web", "@react-native/normalize-colors",
|
|
208
227
|
"inline-style-prefixer/lib/createPrefixer",
|
|
209
228
|
"inline-style-prefixer/lib/plugins/crossFade",
|
|
@@ -326,15 +345,23 @@ export default defineConfig({
|
|
|
326
345
|
content: `import "@lotics/ui/index.css";
|
|
327
346
|
import "@lotics/ui/fonts.css";
|
|
328
347
|
import { PortalHost } from "@lotics/ui/portal";
|
|
348
|
+
import { LoticsLocaleProvider, vi } from "@lotics/ui/locale";
|
|
329
349
|
import { mount } from "@lotics/app-sdk";
|
|
330
350
|
import App from "./App";
|
|
331
351
|
|
|
332
352
|
// PortalHost is the render target for @lotics/ui overlays (Popover, Tooltip,
|
|
333
353
|
// Dialog). Without it, Portal renders nothing — keep it wrapping the app.
|
|
354
|
+
//
|
|
355
|
+
// LoticsLocaleProvider supplies the kit's own strings (Pagination, sort
|
|
356
|
+
// headers, select-all, the Drawer's record nav…) in one language — Vietnamese
|
|
357
|
+
// by default, since Lotics apps are Vietnamese. Swap to \`en\` (or your own
|
|
358
|
+
// LoticsLocale pack) for another language; remove it to fall back to English.
|
|
334
359
|
mount(
|
|
335
|
-
<
|
|
336
|
-
<
|
|
337
|
-
|
|
360
|
+
<LoticsLocaleProvider locale={vi}>
|
|
361
|
+
<PortalHost>
|
|
362
|
+
<App />
|
|
363
|
+
</PortalHost>
|
|
364
|
+
</LoticsLocaleProvider>,
|
|
338
365
|
);
|
|
339
366
|
`,
|
|
340
367
|
},
|