@burdenoff/fe-libs 2026.1021.12 → 2026.1021.14

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The page-context keys the assistant sandbox actually reads, and the host-side
3
+ * filter that keeps every other key in the browser (G52, PR-AI-03 / BOFF-3908).
4
+ *
5
+ * The sandbox (`ai-assistant`) reads `pageContext` in exactly two places:
6
+ *
7
+ * - `src/context-loader.ts` `ALLOWED_CONTEXT_KEYS` — the keys `injectPageContext`
8
+ * writes into the session for the model. Any other key is skipped there.
9
+ * - `src/context-redaction.ts` `CONTEXT_ID_KEYS` — the opaque ids whose values
10
+ * are redacted from the assistant's replies. `organizationId` is on this list
11
+ * but NOT the first, so it must still be forwarded: dropping it would stop the
12
+ * sandbox from redacting an organisation id the model learnt from a tool call.
13
+ *
14
+ * Both lists are private Sets in that repo, and ai-assistant cannot import
15
+ * fe-libs, so they are mirrored here and pinned by a drift test against
16
+ * `__fixtures__/aiAssistantContextKeys.json`, which
17
+ * `scripts/assistant-context-keys/vendor.ts` cuts from an ai-assistant checkout
18
+ * (it records the commit and each source file's SHA-256). Re-vendor, then
19
+ * update these lists, whenever either Set changes.
20
+ *
21
+ * The filter is defence in depth: the sandbox already drops unknown keys before
22
+ * the model sees them, so this changes nothing the model receives. It stops the
23
+ * browser from sending data nobody reads (the user agent, a timestamp, anything
24
+ * a product adds by accident) through the gateway and the sandbox proxy.
25
+ */
26
+ /** Mirror of ai-assistant `ALLOWED_CONTEXT_KEYS`: the keys the model is shown. */
27
+ export declare const ASSISTANT_MODEL_CONTEXT_KEYS: readonly ["currentUrl", "currentPage", "entityType", "entityId", "entityName", "pageTitle", "pageType", "activeTab", "activeTool", "selectedText", "workspaceId", "workspaceName", "projectId", "projectName", "platform", "version", "viewportSize", "vibeId", "vibeName", "vibePath", "sessionId", "sessionName", "agentId", "agentName"];
28
+ /** Mirror of ai-assistant `CONTEXT_ID_KEYS`: the ids redacted from replies. */
29
+ export declare const ASSISTANT_REDACTED_CONTEXT_ID_KEYS: readonly ["workspaceId", "organizationId", "projectId", "entityId", "vibeId", "sessionId", "agentId"];
30
+ export type AssistantPageContextKey = (typeof ASSISTANT_MODEL_CONTEXT_KEYS)[number] | (typeof ASSISTANT_REDACTED_CONTEXT_ID_KEYS)[number];
31
+ /**
32
+ * Every key a host may send in `pageContext`: the union of the two server
33
+ * lists, in a stable order (model keys first, then the redaction-only ones).
34
+ */
35
+ export declare const ASSISTANT_PAGE_CONTEXT_KEYS: readonly AssistantPageContextKey[];
36
+ /** Whether the sandbox reads `key` from `pageContext`. */
37
+ export declare function isAssistantPageContextKey(key: string): key is AssistantPageContextKey;
38
+ /**
39
+ * The part of a host's page context that goes on the wire: only allow-listed
40
+ * keys, with their values untouched. `undefined` stays `undefined`, so a turn
41
+ * with no context still produces the body it always did; a context with no
42
+ * allow-listed key becomes `{}`, which the sandbox treats as "no context".
43
+ *
44
+ * Own enumerable keys only — an inherited or prototype key is never forwarded.
45
+ */
46
+ export declare function filterAssistantPageContext(pageContext: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> | undefined;
@@ -0,0 +1,46 @@
1
+ //#region src/shared/assistant/pageContextKeys.ts
2
+ var e = [
3
+ "currentUrl",
4
+ "currentPage",
5
+ "entityType",
6
+ "entityId",
7
+ "entityName",
8
+ "pageTitle",
9
+ "pageType",
10
+ "activeTab",
11
+ "activeTool",
12
+ "selectedText",
13
+ "workspaceId",
14
+ "workspaceName",
15
+ "projectId",
16
+ "projectName",
17
+ "platform",
18
+ "version",
19
+ "viewportSize",
20
+ "vibeId",
21
+ "vibeName",
22
+ "vibePath",
23
+ "sessionId",
24
+ "sessionName",
25
+ "agentId",
26
+ "agentName"
27
+ ], t = [
28
+ "workspaceId",
29
+ "organizationId",
30
+ "projectId",
31
+ "entityId",
32
+ "vibeId",
33
+ "sessionId",
34
+ "agentId"
35
+ ], n = Object.freeze(Array.from(new Set([...e, ...t]))), r = new Set(n);
36
+ function i(e) {
37
+ return r.has(e);
38
+ }
39
+ function a(e) {
40
+ if (e === void 0) return;
41
+ let t = {};
42
+ for (let [n, i] of Object.entries(e)) r.has(n) && (t[n] = i);
43
+ return t;
44
+ }
45
+ //#endregion
46
+ export { e as ASSISTANT_MODEL_CONTEXT_KEYS, n as ASSISTANT_PAGE_CONTEXT_KEYS, t as ASSISTANT_REDACTED_CONTEXT_ID_KEYS, a as filterAssistantPageContext, i as isAssistantPageContextKey };
@@ -0,0 +1,56 @@
1
+ import { AnchorHTMLAttributes, ReactNode } from 'react';
2
+ import { DocsEnvironment } from '../config/docsUrls';
3
+ export declare const PUBLIC_PRODUCT_DOCS_URL_QUERY: import('graphql').DocumentNode;
4
+ /** Minimal client surface the registry lookup needs (an ApolloClient satisfies it). */
5
+ export interface DocsRegistryClient {
6
+ query: (options: {
7
+ query: typeof PUBLIC_PRODUCT_DOCS_URL_QUERY;
8
+ variables: {
9
+ slug: string;
10
+ };
11
+ fetchPolicy: 'cache-first';
12
+ }) => Promise<{
13
+ data?: unknown;
14
+ }>;
15
+ }
16
+ /** Read `publicProduct.docsUrl` out of a query result, trusted origins only. */
17
+ export declare function readRegistryDocsUrl(data: unknown): string | null;
18
+ /** The cached registry docsUrl for a slug: string, `null` (none), or undefined (not fetched). */
19
+ export declare function getCachedRegistryDocsUrl(productSlug: string): string | null | undefined;
20
+ /**
21
+ * Look up the registry docsUrl once per slug. Failures resolve to `null` (the
22
+ * catalog default stays in use) and are not cached, so a later mount retries.
23
+ */
24
+ export declare function fetchRegistryDocsUrl(client: DocsRegistryClient, productSlug: string): Promise<string | null>;
25
+ /** Test seam: forget every cached registry answer. */
26
+ export declare function resetRegistryDocsUrlCache(): void;
27
+ export interface UseProductDocsOptions {
28
+ /** Another product's docs instead of the running shell's. */
29
+ productSlug?: string;
30
+ /** Skip the registry lookup and use the static catalog default only. */
31
+ registry?: boolean;
32
+ /** Override the detected environment (tests, previews). */
33
+ environment?: DocsEnvironment;
34
+ }
35
+ /**
36
+ * The environment-correct docs origin for the running shell's product, e.g.
37
+ * `https://docs.vibecontrols.com` or `https://alphadocs.vibecontrols.com`.
38
+ */
39
+ export declare function useProductDocsOrigin(options?: UseProductDocsOptions): string | undefined;
40
+ /**
41
+ * The environment-correct docs URL for a page of the running shell's product.
42
+ * `path` is site-relative (`/users/workflows`); undefined when it is not, or
43
+ * when the product has no trusted docs host.
44
+ */
45
+ export declare function useProductDocsUrl(path?: string, options?: UseProductDocsOptions): string | undefined;
46
+ export interface DocsLinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'target' | 'rel'>, UseProductDocsOptions {
47
+ /** Site-relative docs path, e.g. `/users/workflows`. Defaults to the docs home. */
48
+ path?: string;
49
+ children: ReactNode;
50
+ }
51
+ /**
52
+ * A new-tab link to the product's docs for the current environment. Renders
53
+ * its children without a link when no trusted docs URL resolves, so a page
54
+ * never shows a dead "Learn more".
55
+ */
56
+ export declare function DocsLink({ path, productSlug, registry, environment, children, ...anchorProps }: DocsLinkProps): import('react/jsx-runtime').JSX.Element;
@@ -0,0 +1,97 @@
1
+ import { normalizeProductSlug as e } from "../config/productSlug.js";
2
+ import { buildProductDocsUrl as t, resolveProductDocsOrigin as n, toTrustedDocsOrigin as r } from "../config/docsUrls.js";
3
+ import { useSafeGateway as i } from "../graphql/GatewayContext.js";
4
+ import { useAppShell as a } from "../providers/shell/AppShellContext.js";
5
+ import { useEffect as o, useState as s } from "react";
6
+ import { Fragment as c, jsx as l } from "react/jsx-runtime";
7
+ import { gql as u } from "@apollo/client/core";
8
+ //#region src/shared/components/DocsLink.tsx
9
+ var d = u`
10
+ query PublicProductDocsUrl($slug: String!) {
11
+ publicProduct(slug: $slug) {
12
+ docsUrl
13
+ }
14
+ }
15
+ `, f = /* @__PURE__ */ new Map(), p = /* @__PURE__ */ new Map();
16
+ function m(e) {
17
+ if (!e || typeof e != "object") return null;
18
+ let t = e.publicProduct;
19
+ if (!t || typeof t != "object") return null;
20
+ let n = t.docsUrl;
21
+ return typeof n == "string" && r(n) ? n : null;
22
+ }
23
+ function h(t) {
24
+ return f.get(e(t));
25
+ }
26
+ function g(t, n) {
27
+ let r = e(n), i = f.get(r);
28
+ if (i !== void 0) return Promise.resolve(i);
29
+ let a = p.get(r);
30
+ if (a) return a;
31
+ let o = t.query({
32
+ query: d,
33
+ variables: { slug: r },
34
+ fetchPolicy: "cache-first"
35
+ }).then((e) => {
36
+ let t = m(e.data);
37
+ return f.set(r, t), t;
38
+ }).catch(() => null).finally(() => {
39
+ p.delete(r);
40
+ });
41
+ return p.set(r, o), o;
42
+ }
43
+ function _(e, t) {
44
+ let n = i(), [r, a] = s(() => h(e) ?? null);
45
+ return o(() => {
46
+ if (!t || !n) return;
47
+ let r = h(e);
48
+ if (r !== void 0) {
49
+ a(r);
50
+ return;
51
+ }
52
+ a(null);
53
+ let i = !0;
54
+ return g(n.getClient("global"), e).then((e) => {
55
+ i && a(e);
56
+ }), () => {
57
+ i = !1;
58
+ };
59
+ }, [
60
+ t,
61
+ n,
62
+ e
63
+ ]), t ? r : null;
64
+ }
65
+ function v(e = {}) {
66
+ let t = a(), r = e.productSlug ?? t.productSlug;
67
+ return n(r, {
68
+ registryDocsUrl: _(r, e.registry !== !1),
69
+ domain: e.productSlug ? void 0 : t.domain,
70
+ environment: e.environment
71
+ });
72
+ }
73
+ function y(e, n = {}) {
74
+ let r = a(), i = n.productSlug ?? r.productSlug;
75
+ return t(i, e, {
76
+ registryDocsUrl: _(i, n.registry !== !1),
77
+ domain: n.productSlug ? void 0 : r.domain,
78
+ environment: n.environment
79
+ });
80
+ }
81
+ function b({ path: e, productSlug: t, registry: n, environment: r, children: i, ...a }) {
82
+ let o = y(e, {
83
+ productSlug: t,
84
+ registry: n,
85
+ environment: r
86
+ });
87
+ return o ? /* @__PURE__ */ l("a", {
88
+ ...a,
89
+ href: o,
90
+ target: "_blank",
91
+ rel: "noopener noreferrer",
92
+ "data-docs-link": "",
93
+ children: i
94
+ }) : /* @__PURE__ */ l(c, { children: i });
95
+ }
96
+ //#endregion
97
+ export { b as DocsLink, d as PUBLIC_PRODUCT_DOCS_URL_QUERY, g as fetchRegistryDocsUrl, m as readRegistryDocsUrl, v as useProductDocsOrigin, y as useProductDocsUrl };
@@ -30,6 +30,8 @@ export type { QuotaUpgradeBridgeProps, QuotaUpgradeBridgeOptions, QuotaUpgradeCo
30
30
  export { getSSORedirectUrl, redirectToSSO, SSORedirectSpinner, checkSLO, checkRemoteSLO, clearLocalAuthState, } from './SSORedirectGuard';
31
31
  export { MAIN_CONTENT_ID, MainLandmark, PublicLayout, SkipToContentLink, focusSkipTarget, useInsideMainLandmark, } from './SkipToContent';
32
32
  export type { MainLandmarkProps, PublicLayoutProps, SkipTarget, SkipToContentLinkProps, } from './SkipToContent';
33
+ export { DocsLink, useProductDocsOrigin, useProductDocsUrl, fetchRegistryDocsUrl, readRegistryDocsUrl, PUBLIC_PRODUCT_DOCS_URL_QUERY, } from './DocsLink';
34
+ export type { DocsLinkProps, DocsRegistryClient, UseProductDocsOptions } from './DocsLink';
33
35
  export { LandingOrDashboardRedirect, LandingGateView, resolveLandingGate, hasStoredValidSession, } from './LandingOrDashboardRedirect';
34
36
  export type { LandingOrDashboardRedirectProps, LandingGateViewProps, LandingGateDecision, LandingGateInput, } from './LandingOrDashboardRedirect';
35
37
  export { AssistantWidget, ASSISTANT_WIDGET_TOGGLE_EVENT } from './assistantWidget/AssistantWidget';
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Environment-aware product documentation URLs (G91).
3
+ *
4
+ * Every product publishes its docs at `docs.<product-domain>` in production
5
+ * and `alphadocs.<product-domain>` in alpha (behind Cloudflare Access, like
6
+ * every other alpha host). In-app "Learn more" and Help links must follow the
7
+ * environment the shell is running in: an alpha shell that links to the prod
8
+ * docs shows readers documentation for a build they are not using.
9
+ *
10
+ * Resolution, highest precedence first:
11
+ * 1. The product registry `docsUrl` (`publicProduct(slug).docsUrl`), when it
12
+ * is an https URL on a trusted product domain.
13
+ * 2. The static catalog: the product's production app host with `app.`
14
+ * swapped for `docs.`.
15
+ * 3. `docs.<domain>` from the shell's own `domain`, for a product the static
16
+ * catalog does not know yet.
17
+ *
18
+ * The chosen origin is then canonicalised (legacy docs hosts are mapped to
19
+ * their canonical twin — FluidGrids' canonical domain is fluidgrids.ai) and
20
+ * converted to the current environment (`docs.` ↔ `alphadocs.`).
21
+ *
22
+ * @module @burdenoff/fe-libs/shared/config/docsUrls
23
+ */
24
+ /** The docs estate a link should point at. Local dev servers read the prod docs. */
25
+ export type DocsEnvironment = 'prod' | 'alpha';
26
+ /**
27
+ * Legacy docs hosts and the canonical host each one 301s to. A registry row or
28
+ * shell config that still carries the legacy host resolves straight to the
29
+ * canonical one, so a link never relies on the redirect.
30
+ */
31
+ export declare const CANONICAL_DOCS_HOSTS: Readonly<Record<string, string>>;
32
+ /**
33
+ * Parse a docs base URL and return its `https://host` origin, or null when it
34
+ * is not a usable docs origin: not https, not on a known product domain,
35
+ * or not parseable. Registry values are user-editable admin data, so an
36
+ * arbitrary host must never become an in-app link.
37
+ */
38
+ export declare function toTrustedDocsOrigin(url: string | null | undefined): string | null;
39
+ /** Map a legacy docs origin to its canonical twin; other origins are returned unchanged. */
40
+ export declare function canonicalizeDocsOrigin(origin: string): string;
41
+ /**
42
+ * Convert a docs origin to the given environment: `docs.x` ↔ `alphadocs.x`.
43
+ * An origin that follows neither convention is returned unchanged, because
44
+ * guessing an alpha twin for it could produce a host that does not exist.
45
+ */
46
+ export declare function toEnvironmentDocsOrigin(origin: string, environment: DocsEnvironment): string;
47
+ /**
48
+ * The default prod docs origin for a product from the static catalog
49
+ * (`app.<domain>` → `docs.<domain>`), canonicalised. Undefined when the
50
+ * catalog does not know the product.
51
+ */
52
+ export declare function getCatalogDocsOrigin(productSlug: string): string | undefined;
53
+ /**
54
+ * Which docs estate the running shell belongs to.
55
+ *
56
+ * - Desktop (Electron): the baked runtime config's `environment`.
57
+ * - Capacitor: the build's deploy target (the WebView origin is always localhost).
58
+ * - Web: `alphaapp.*` / `alphaadmin.*` hosts are alpha; everything else is prod.
59
+ * A localhost dev server built for alpha (`VITE_DEPLOY_ENV=alpha` or
60
+ * `VITE_GATEWAY_TARGET=alpha`) reads the alpha docs; otherwise prod, because
61
+ * docs are not served by the local stack.
62
+ */
63
+ export declare function resolveDocsEnvironment(hostname?: string): DocsEnvironment;
64
+ export interface ResolveDocsOriginOptions {
65
+ /** The registry's `docsUrl` for the product, when the shell has it. */
66
+ registryDocsUrl?: string | null;
67
+ /** The shell's own domain (AppShellConfig `domain`), for products missing from the catalog. */
68
+ domain?: string;
69
+ /** Override the detected environment (tests, previews). */
70
+ environment?: DocsEnvironment;
71
+ }
72
+ /**
73
+ * The environment-correct docs origin for a product, e.g.
74
+ * `https://alphadocs.vibecontrols.com` on alphaapp.vibecontrols.com.
75
+ * Undefined only when no source names a trusted docs host.
76
+ */
77
+ export declare function resolveProductDocsOrigin(productSlug: string, options?: ResolveDocsOriginOptions): string | undefined;
78
+ /**
79
+ * Normalise a docs path: must be site-relative. Absolute URLs and
80
+ * protocol-relative `//host` paths are rejected (they would let a caller turn
81
+ * a "docs" link into an arbitrary external link). Query and hash are kept.
82
+ */
83
+ export declare function normalizeDocsPath(path: string | undefined): string | null;
84
+ /**
85
+ * Full docs URL for a product page, e.g.
86
+ * `buildProductDocsUrl('fluidgrids', '/users/workflows')` →
87
+ * `https://docs.fluidgrids.ai/users/workflows` in prod and
88
+ * `https://alphadocs.fluidgrids.ai/users/workflows` in alpha.
89
+ * Undefined when there is no trusted docs origin or the path is not site-relative.
90
+ */
91
+ export declare function buildProductDocsUrl(productSlug: string, path?: string, options?: ResolveDocsOriginOptions): string | undefined;
@@ -0,0 +1,70 @@
1
+ import { isCapacitorNative as e } from "../native/capacitor.js";
2
+ import { getDesktopRuntimeConfig as t } from "../native/desktop.js";
3
+ import { ALL_PRODUCT_DOMAINS as n, ALPHA_HOSTS as r, getCurrentHostname as i, resolveNativeDeployTarget as a } from "./gatewayUrls.js";
4
+ import { legacyProductSlug as o, normalizeProductSlug as s } from "./productSlug.js";
5
+ import { PRODUCT_URLS as c } from "./products.js";
6
+ //#region src/shared/config/docsUrls.ts
7
+ var l = "docs.", u = "alphadocs.", d = {
8
+ "docs.fluidgrids.com": "docs.fluidgrids.ai",
9
+ "alphadocs.fluidgrids.com": "alphadocs.fluidgrids.ai"
10
+ }, f = new Set(n);
11
+ function p(e) {
12
+ let t = e.toLowerCase();
13
+ for (let e of f) if (t === e || t.endsWith(`.${e}`)) return !0;
14
+ return !1;
15
+ }
16
+ function m(e) {
17
+ if (typeof e != "string" || e.trim() === "") return null;
18
+ let t;
19
+ try {
20
+ t = new URL(e.trim());
21
+ } catch {
22
+ return null;
23
+ }
24
+ return t.protocol !== "https:" || t.username || t.password || t.port || !p(t.hostname) ? null : `https://${t.hostname.toLowerCase()}`;
25
+ }
26
+ function h(e) {
27
+ let t = d[e.replace(/^https:\/\//, "")];
28
+ return t ? `https://${t}` : e;
29
+ }
30
+ function g(e, t) {
31
+ let n = e.replace(/^https:\/\//, ""), r = null;
32
+ return n.startsWith(u) ? r = n.slice(10) : n.startsWith(l) && (r = n.slice(5)), r ? `https://${t === "alpha" ? u : l}${r}` : e;
33
+ }
34
+ function _(e) {
35
+ let t = c.production, n = s(e), r = t[n] ?? t[o(n)];
36
+ if (!r) return;
37
+ let i;
38
+ try {
39
+ i = new URL(r).hostname;
40
+ } catch {
41
+ return;
42
+ }
43
+ return h(`https://${l}${i.startsWith("app.") ? i.slice(4) : i}`);
44
+ }
45
+ function v(n = i()) {
46
+ let o = t()?.environment;
47
+ if (o) return o === "alpha" ? "alpha" : "prod";
48
+ if (e()) return a() === "alpha" ? "alpha" : "prod";
49
+ let s = n.toLowerCase(), c = () => a() === "alpha" ? "alpha" : "prod";
50
+ return r.has(s) ? "alpha" : s.endsWith(".local.burdenoff.com") ? c() : s.startsWith("alpha") && p(s) ? "alpha" : p(s) ? "prod" : c();
51
+ }
52
+ function y(e, t = {}) {
53
+ let n = t.domain ? m(`https://${l}${t.domain.replace(/^www\./, "")}`) : null, r = m(t.registryDocsUrl) ?? _(e) ?? n ?? void 0;
54
+ if (!r) return;
55
+ let i = t.environment ?? v();
56
+ return g(h(r), i);
57
+ }
58
+ function b(e) {
59
+ if (e === void 0 || e === "") return "";
60
+ let t = e.trim();
61
+ return /^[a-z][a-z0-9+.-]*:/i.test(t) || t.startsWith("//") || t.includes("\\") ? null : t.startsWith("/") || t.startsWith("#") || t.startsWith("?") ? t : `/${t}`;
62
+ }
63
+ function x(e, t, n = {}) {
64
+ let r = y(e, n);
65
+ if (!r) return;
66
+ let i = b(t);
67
+ if (i !== null) return `${r}${i}`;
68
+ }
69
+ //#endregion
70
+ export { d as CANONICAL_DOCS_HOSTS, x as buildProductDocsUrl, h as canonicalizeDocsOrigin, _ as getCatalogDocsOrigin, b as normalizeDocsPath, v as resolveDocsEnvironment, y as resolveProductDocsOrigin, g as toEnvironmentDocsOrigin, m as toTrustedDocsOrigin };
@@ -68,6 +68,8 @@ export { resolveGatewayUrls } from './gatewayUrls';
68
68
  export type { RuntimeGatewayUrls } from './gatewayUrls';
69
69
  export { resolveAuthBridgeConfig, resolveShellAuthBridgeConfig } from './authBridgeUrls';
70
70
  export type { RuntimeAuthBridgeConfig } from './authBridgeUrls';
71
+ export { CANONICAL_DOCS_HOSTS, buildProductDocsUrl, canonicalizeDocsOrigin, getCatalogDocsOrigin, normalizeDocsPath, resolveDocsEnvironment, resolveProductDocsOrigin, toEnvironmentDocsOrigin, toTrustedDocsOrigin, } from './docsUrls';
72
+ export type { DocsEnvironment, ResolveDocsOriginOptions } from './docsUrls';
71
73
  export { PRODUCTS, PRODUCT_URLS, BOTMANOR_PRODUCT_SLUG, getProductUrl, getProductsWithUrls, findProductConfig, } from './products';
72
74
  export type { ProductConfig } from './products';
73
75
  export { PRODUCT_SLUG_RENAMES, PRODUCT_SLUG_EMIT, normalizeProductSlug, legacyProductSlug, isSameProductSlug, emitProductSlug, } from './productSlug';