@fayz-ai/plugin-inventory 0.8.0 → 0.8.1

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.
@@ -3,6 +3,8 @@ import type { InventoryPluginLabels } from './index';
3
3
  import type { InventoryDataProvider } from './data/types';
4
4
  import type { InventoryUIState } from './store';
5
5
  export interface InventoryModules {
6
+ /** Product catalogue screens. Off when the host registers products elsewhere. */
7
+ products: boolean;
6
8
  recipes: boolean;
7
9
  stockLocations: boolean;
8
10
  batchTracking: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"InventoryContext.d.ts","sourceRoot":"","sources":["../src/InventoryContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAA;IACjC,SAAS,EAAE,cAAc,EAAE,CAAA;CAC5B;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"InventoryContext.d.ts","sourceRoot":"","sources":["../src/InventoryContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAA;AACxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AACpD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,aAAa,EAAE,OAAO,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAE9C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,gBAAgB,CAAA;IACzB,MAAM,EAAE,qBAAqB,CAAA;IAC7B,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,YAAY,EAAE,iBAAiB,EAAE,CAAA;IACjC,SAAS,EAAE,cAAc,EAAE,CAAA;CAC5B;AAID,eAAO,MAAM,wBAAwB;;;;;EAAsB,CAAA;AAC3D,eAAO,MAAM,kBAAkB,+BAAgB,CAAA;AAC/C,eAAO,MAAM,oBAAoB,6BAAkB,CAAA;AACnD,eAAO,MAAM,iBAAiB,oDAAe,CAAA;AAE7C,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"InventoryPage.d.ts","sourceRoot":"","sources":["../src/InventoryPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,eAAe,CAAA;AA+CzE,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IACrE,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACjC,qBA+GA"}
1
+ {"version":3,"file":"InventoryPage.d.ts","sourceRoot":"","sources":["../src/InventoryPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAA;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AAE/C,OAAO,EAA4B,KAAK,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAqB,MAAM,eAAe,CAAA;AA+CzE,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;IACrE,MAAM,EAAE,uBAAuB,CAAA;IAC/B,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,KAAK,EAAE,QAAQ,CAAC,gBAAgB,CAAC,CAAA;IACjC,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACjC,qBAqHA"}
@@ -1 +1 @@
1
- {"version":3,"file":"InventoryGeneralSettings.d.ts","sourceRoot":"","sources":["../../src/components/InventoryGeneralSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,wBAAgB,wBAAwB,sBAqBvC"}
1
+ {"version":3,"file":"InventoryGeneralSettings.d.ts","sourceRoot":"","sources":["../../src/components/InventoryGeneralSettings.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAgBzB,wBAAgB,wBAAwB,sBAsBvC"}
package/dist/index.cjs CHANGED
@@ -173,7 +173,8 @@ function ProductListView({ onNew, onEdit }) {
173
173
  onFacetChange: (_field, value) => setTypeFilter(value),
174
174
  onNew,
175
175
  addLabel: t2("inventory.productList.newProduct"),
176
- onRowClick: (row) => onEdit?.(row.id)
176
+ onRowClick: (row) => onEdit?.(row.id),
177
+ feature: "inventory"
177
178
  }
178
179
  );
179
180
  }
@@ -182,6 +183,7 @@ function ProductCrudForm({ editId, onSaved }) {
182
183
  const { productTypes, currency } = useInventoryConfig();
183
184
  const provider = useInventoryProvider();
184
185
  const createProduct = useInventoryStore((s) => s.createProduct);
186
+ const guardProducts = saas.useLimitGuard("products");
185
187
  const isEdit = !!editId;
186
188
  const [initialData, setInitialData] = React2__default.default.useState(isEdit ? null : {});
187
189
  const [headerName, setHeaderName] = React2__default.default.useState("");
@@ -233,7 +235,9 @@ function ProductCrudForm({ editId, onSaved }) {
233
235
  if (isEdit && editId) {
234
236
  await provider.updateProduct(editId, payload);
235
237
  } else {
238
+ if (await guardProducts() === "blocked") return;
236
239
  await createProduct(payload);
240
+ saas.invalidateLimit("products");
237
241
  }
238
242
  onSaved?.();
239
243
  }
@@ -809,11 +813,11 @@ function RecipesView({ onNew, onView }) {
809
813
  {
810
814
  title: t2("inventory.recipes.title"),
811
815
  subtitle: t2("inventory.recipes.productionFormulas", { count: String(recipes.length) }),
812
- actions: onNew && /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: onNew, className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors", children: [
816
+ actions: onNew && /* @__PURE__ */ jsxRuntime.jsx(saas.PermissionGate, { feature: "inventory", action: "create", children: /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: onNew, className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors", children: [
813
817
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
814
818
  " ",
815
819
  t2("inventory.recipes.newRecipe")
816
- ] })
820
+ ] }) })
817
821
  }
818
822
  ),
819
823
  recipesLoading ? /* @__PURE__ */ jsxRuntime.jsx(RecipeSkeleton, {}) : recipes.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-center rounded-lg border-2 border-dashed border-muted", children: [
@@ -872,6 +876,7 @@ function RecipeFormView({ onSaved }) {
872
876
  const t2 = core.useTranslation();
873
877
  const provider = useInventoryProvider();
874
878
  const createRecipe = useInventoryStore((s) => s.createRecipe);
879
+ const guardRecipes = saas.useLimitGuard("recipes");
875
880
  const [name, setName] = React2.useState("");
876
881
  const [description, setDescription] = React2.useState("");
877
882
  const [productId, setProductId] = React2.useState("");
@@ -895,6 +900,7 @@ function RecipeFormView({ onSaved }) {
895
900
  ui.toast.error(t2("common.formIncomplete"));
896
901
  return;
897
902
  }
903
+ if (await guardRecipes() === "blocked") return;
898
904
  setSaving(true);
899
905
  try {
900
906
  const recipe = await createRecipe({
@@ -912,6 +918,7 @@ function RecipeFormView({ onSaved }) {
912
918
  notes: i.notes || void 0
913
919
  }))
914
920
  });
921
+ saas.invalidateLimit("recipes");
915
922
  onSaved?.(recipe.id);
916
923
  } finally {
917
924
  setSaving(false);
@@ -1222,28 +1229,31 @@ function RecipeDetailView({ recipeId, onBack }) {
1222
1229
  ] })
1223
1230
  ] });
1224
1231
  }
1232
+ var INVENTORY_DEFAULTS = {
1233
+ lowStockAlerts: true,
1234
+ requireReason: true,
1235
+ autoDeduct: false,
1236
+ requireSku: false,
1237
+ allowNegative: false,
1238
+ lowStockEmail: false,
1239
+ expiryWarnings: true
1240
+ };
1225
1241
  function InventoryGeneralSettings() {
1226
1242
  const t2 = core.useTranslation();
1243
+ const s = saas.useTenantPluginSettings("inventory", INVENTORY_DEFAULTS);
1227
1244
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
1228
1245
  /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.stockManagement"), description: t2("inventory.settings.stockManagementDesc"), children: [
1229
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: true, onChange: () => {
1230
- } }),
1231
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: true, onChange: () => {
1232
- } }),
1233
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: false, onChange: () => {
1234
- } })
1246
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockAlerts"), description: t2("inventory.settings.lowStockAlertsDesc"), checked: s.get("lowStockAlerts"), onChange: (v) => s.set("lowStockAlerts", v) }),
1247
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireReason"), description: t2("inventory.settings.requireReasonDesc"), checked: s.get("requireReason"), onChange: (v) => s.set("requireReason", v) }),
1248
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.autoDeduct"), description: t2("inventory.settings.autoDeductDesc"), checked: s.get("autoDeduct"), onChange: (v) => s.set("autoDeduct", v) })
1235
1249
  ] }),
1236
1250
  /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.products"), description: t2("inventory.settings.productsDesc"), children: [
1237
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: false, onChange: () => {
1238
- } }),
1239
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: false, onChange: () => {
1240
- } })
1251
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.requireSku"), description: t2("inventory.settings.requireSkuDesc"), checked: s.get("requireSku"), onChange: (v) => s.set("requireSku", v) }),
1252
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.allowNegative"), description: t2("inventory.settings.allowNegativeDesc"), checked: s.get("allowNegative"), onChange: (v) => s.set("allowNegative", v) })
1241
1253
  ] }),
1242
1254
  /* @__PURE__ */ jsxRuntime.jsxs(saas.SettingsGroup, { title: t2("inventory.settings.notifications"), description: t2("inventory.settings.notificationsDesc"), children: [
1243
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: false, onChange: () => {
1244
- } }),
1245
- /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: true, onChange: () => {
1246
- } })
1255
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.lowStockEmail"), description: t2("inventory.settings.lowStockEmailDesc"), checked: s.get("lowStockEmail"), onChange: (v) => s.set("lowStockEmail", v) }),
1256
+ /* @__PURE__ */ jsxRuntime.jsx(saas.ToggleRow, { label: t2("inventory.settings.expiryWarnings"), description: t2("inventory.settings.expiryWarningsDesc"), checked: s.get("expiryWarnings"), onChange: (v) => s.set("expiryWarnings", v) })
1247
1257
  ] })
1248
1258
  ] });
1249
1259
  }
@@ -1285,7 +1295,7 @@ function InventoryOnboarding({ onComplete }) {
1285
1295
  function buildNav(config, view, navigate, t2) {
1286
1296
  const items = [
1287
1297
  { id: "dashboard", label: t2("inventory.nav.dashboard"), icon: "BarChart3", active: view === "dashboard", onClick: () => navigate("dashboard") },
1288
- {
1298
+ ...config.modules.products ? [{
1289
1299
  id: "products",
1290
1300
  label: t2("inventory.nav.products"),
1291
1301
  icon: "Package",
@@ -1294,7 +1304,7 @@ function buildNav(config, view, navigate, t2) {
1294
1304
  { id: "products-new", label: t2("inventory.nav.new"), active: view === "products-new", onClick: () => navigate("products-new") },
1295
1305
  { id: "products-list", label: t2("inventory.nav.list"), active: view === "products-list", onClick: () => navigate("products-list") }
1296
1306
  ]
1297
- },
1307
+ }] : [],
1298
1308
  {
1299
1309
  id: "stock",
1300
1310
  label: t2("inventory.nav.stock"),
@@ -1345,13 +1355,15 @@ function InventoryPage({ config, provider, store, registries }) {
1345
1355
  const nav = buildNav(config, view, navigate, t2);
1346
1356
  const quickActions = React2.useMemo(() => {
1347
1357
  const actions = [
1348
- {
1358
+ // Dropped with the catalogue module: offering "new product" while the
1359
+ // screen is gone would dead-end the user.
1360
+ ...config.modules.products ? [{
1349
1361
  id: "new-product",
1350
1362
  label: t2("inventory.quickActions.newProduct"),
1351
1363
  icon: "Package",
1352
1364
  description: t2("inventory.quickActions.newProductDesc"),
1353
1365
  action: () => navigate("products-new")
1354
- },
1366
+ }] : [],
1355
1367
  {
1356
1368
  id: "stock-entry",
1357
1369
  label: t2("inventory.quickActions.stockEntry"),
@@ -1388,9 +1400,13 @@ function InventoryPage({ config, provider, store, registries }) {
1388
1400
  ] }) }) });
1389
1401
  }
1390
1402
  const renderView = saas.createViewRouter([
1391
- { id: "products-list", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
1392
- { id: "products-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
1393
- { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) },
1403
+ // Routes go with the module, not just the nav entry: leaving them mounted
1404
+ // keeps a second product form reachable by URL.
1405
+ ...config.modules.products ? [
1406
+ { id: "products-list", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductListView, { onNew: () => navigate("products-new"), onEdit: (id) => navigate(`products-edit:${id}`) }) },
1407
+ { id: "products-new", render: () => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { onSaved: () => navigate("products-list") }) },
1408
+ { id: "products-edit", render: ({ id }) => /* @__PURE__ */ jsxRuntime.jsx(ProductCrudForm, { editId: id, onSaved: () => navigate("products-list") }) }
1409
+ ] : [],
1394
1410
  { id: "stock-entry", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "entry", onSaved: () => navigate("stock-history") }) },
1395
1411
  { id: "stock-exit", render: () => /* @__PURE__ */ jsxRuntime.jsx(StockMovementView, { defaultType: "exit", onSaved: () => navigate("stock-history") }) },
1396
1412
  { id: "stock-history", render: () => /* @__PURE__ */ jsxRuntime.jsx(MovementHistoryView, { onViewDetail: (id) => navigate(`stock-detail:${id}`) }) },
@@ -2747,6 +2763,7 @@ var DEFAULT_PRODUCT_TYPES = [
2747
2763
  function resolveConfig(options) {
2748
2764
  return {
2749
2765
  modules: {
2766
+ products: options?.modules?.products !== false,
2750
2767
  recipes: options?.modules?.recipes !== false,
2751
2768
  stockLocations: options?.modules?.stockLocations !== false,
2752
2769
  batchTracking: options?.modules?.batchTracking ?? false
@@ -2780,6 +2797,40 @@ function createInventoryPlugin(options) {
2780
2797
  { id: "inventory", label: config.labels.pageTitle, group: config.labels.pageTitle },
2781
2798
  ...config.modules.recipes ? [{ id: "inventory.recipes", label: config.labels.recipes ?? "Recipes", group: config.labels.pageTitle }] : []
2782
2799
  ],
2800
+ queryEntities: [
2801
+ {
2802
+ key: "inventory:products",
2803
+ writable: true,
2804
+ entity: {
2805
+ name: "Product",
2806
+ namePlural: "Products",
2807
+ icon: "Package",
2808
+ limitKey: "products",
2809
+ permission: { feature: "inventory", action: "read" },
2810
+ fields: [
2811
+ { key: "name", label: "Name", type: "text", required: true, searchable: true },
2812
+ { key: "kind", label: "Kind (sale/ingredient/asset)", type: "text" },
2813
+ { key: "price", label: "Price", type: "number" },
2814
+ { key: "cost", label: "Cost", type: "number" },
2815
+ { key: "sku", label: "SKU", type: "text", searchable: true },
2816
+ { key: "isActive", label: "Active", type: "boolean" },
2817
+ { key: "createdAt", label: "Created at", type: "text" }
2818
+ ],
2819
+ data: {
2820
+ table: "products",
2821
+ tenantScoped: true,
2822
+ archetype: "product",
2823
+ archetypeKind: "sale",
2824
+ searchColumns: ["name", "sku"],
2825
+ defaults: { kind: "sale", status: "active", is_active: true }
2826
+ }
2827
+ }
2828
+ }
2829
+ ],
2830
+ declaredLimits: [
2831
+ { key: "products", label: "Products", table: "products" },
2832
+ ...config.modules.recipes ? [{ key: "recipes", label: config.labels.recipes ?? "Recipes", table: "plg_inventory_recipes" }] : []
2833
+ ],
2783
2834
  navigation: [
2784
2835
  {
2785
2836
  section: options?.navSection ?? "main",