@timmbr/cms 0.0.0-stage → 1.0.0

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.
Files changed (37) hide show
  1. package/README.md +96 -2
  2. package/dist/api/categories.d.ts +7 -0
  3. package/dist/api/categories.d.ts.map +1 -0
  4. package/dist/api/categories.js +28 -0
  5. package/dist/api/core-client.d.ts +8 -0
  6. package/dist/api/core-client.d.ts.map +1 -0
  7. package/dist/api/core-client.js +30 -0
  8. package/dist/components/GlobalFooter.d.ts +11 -0
  9. package/dist/components/GlobalFooter.d.ts.map +1 -0
  10. package/dist/components/GlobalFooter.js +44 -0
  11. package/dist/components/GlobalHeader.d.ts +13 -0
  12. package/dist/components/GlobalHeader.d.ts.map +1 -0
  13. package/dist/components/GlobalHeader.js +29 -0
  14. package/dist/components/StorefrontFooter.d.ts +14 -0
  15. package/dist/components/StorefrontFooter.d.ts.map +1 -0
  16. package/dist/components/StorefrontFooter.js +107 -0
  17. package/dist/components/StorefrontHeader.d.ts +14 -0
  18. package/dist/components/StorefrontHeader.d.ts.map +1 -0
  19. package/dist/components/StorefrontHeader.js +136 -0
  20. package/dist/copy/chrome.d.ts +126 -0
  21. package/dist/copy/chrome.d.ts.map +1 -0
  22. package/dist/copy/chrome.js +99 -0
  23. package/dist/fallbacks/categories.d.ts +4 -0
  24. package/dist/fallbacks/categories.d.ts.map +1 -0
  25. package/dist/fallbacks/categories.js +3 -0
  26. package/dist/hooks/useStorefrontSearch.d.ts +19 -0
  27. package/dist/hooks/useStorefrontSearch.d.ts.map +1 -0
  28. package/dist/hooks/useStorefrontSearch.js +73 -0
  29. package/dist/index.d.ts +15 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +8 -0
  32. package/dist/mappers/nav.d.ts +25 -0
  33. package/dist/mappers/nav.d.ts.map +1 -0
  34. package/dist/mappers/nav.js +80 -0
  35. package/dist/types/index.d.ts +28 -0
  36. package/dist/types/index.d.ts.map +1 -0
  37. package/package.json +70 -3
package/README.md CHANGED
@@ -1,3 +1,97 @@
1
- # Temporary Holding Version
1
+ # @timmbr/cms
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Storefront chrome integration for Timmbr: category tree fetching, navigation mappers, `GlobalHeader`, `GlobalFooter`, and client components for multi-zone layouts.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @timmbr/cms
9
+ ```
10
+
11
+ Peer dependencies:
12
+ - `@timmbr/ui`
13
+ - `@timmbr/auth`
14
+ - `@timmbr/hooks`
15
+ - `react`, `react-dom`, `next`
16
+
17
+ ## Features
18
+
19
+ - **Category Navigation Tree**: Cached fetching from timmbr-core (`GET /api/v1/categories/tree` via `getCategoryTree`) with graceful fallback.
20
+
21
+ ### Environment (server)
22
+
23
+ Category tree reads run on the server inside `GlobalHeader` / `GlobalFooter`:
24
+
25
+ - **`CORE_API_URL`** or **`NEXT_PUBLIC_API_URL`**: timmbr-core base URL (e.g. `http://localhost:8000`).
26
+ - **`CORE_INTERNAL_KEY`** (optional): when set (32+ chars), sent as `x-internal-key` so core can treat the zone as a trusted storefront client.
27
+
28
+ If neither base URL is set, chrome still renders with an empty category tree. In **development**, `@timmbr/cms` also tries `http://localhost:8000` so catalog/account/checkout zones work without a local `.env` (matching each zone's `env.ts` defaults). For production, set `CORE_API_URL` or `NEXT_PUBLIC_API_URL` on **every zone** that renders `GlobalHeader`.
29
+ - **Nav Mappers**: Transforms category hierarchies into `@timmbr/ui` `NavHeader` and `NavFooter` structures (`toNavTree`, `toCategoryMenus`, `toFooterLinks`).
30
+ - **Zone-Aware Navigation**: Automatic detection of cross-zone links (`catalog`, `shell`, `account`, `checkout`).
31
+ - **Dynamic Footer Social Links**: Pass custom social platforms (`socialLinks` or `social`), auto-resolve branded `@timmbr/icons`, override icons, customize titles, or toggle visibility (`social={false}`). Falls back gracefully to `chromeCopy.footer.socialLinks`.
32
+ - **Unified Chrome Components**:
33
+ - `GlobalHeader`: Async server component fetching category tree and rendering `StorefrontHeader`.
34
+ - `GlobalFooter`: Async server component resolving footer sections and rendering `StorefrontFooter`.
35
+ - `StorefrontHeader`: Client header handling storefront and checkout variants, search suggestions, cart badges, and auth trigger.
36
+ - `StorefrontFooter`: Client footer rendering branding, legal links, office details, and social icons.
37
+ - **Storefront Search**: `useStorefrontSearch` hook wired to recent searches and product suggestions.
38
+
39
+ ## Usage
40
+
41
+ ### Server Component (Shell / Layout)
42
+
43
+ ```tsx
44
+ import { GlobalHeader, GlobalFooter } from '@timmbr/cms';
45
+
46
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
47
+ return (
48
+ <html lang="en">
49
+ <body>
50
+ <GlobalHeader
51
+ variant="store"
52
+ assets={{
53
+ logo: '/images/logo.svg',
54
+ miniLogo: '/images/mini-logo.svg',
55
+ }}
56
+ zoneContext="shell"
57
+ />
58
+ <main>{children}</main>
59
+ <GlobalFooter
60
+ assets={{
61
+ logo: '/images/logo.svg',
62
+ }}
63
+ zoneContext="shell"
64
+ socialLinks={[
65
+ { name: 'instagram', href: 'https://instagram.com/timmbr' },
66
+ { name: 'facebook', href: 'https://facebook.com/timmbr' },
67
+ { name: 'linkedin', href: 'https://linkedin.com/company/timmbr' },
68
+ { name: 'x', href: 'https://x.com/timmbr' },
69
+ ]}
70
+ />
71
+ </body>
72
+ </html>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ### Checkout Layout
78
+
79
+ ```tsx
80
+ import { GlobalHeader } from '@timmbr/cms';
81
+
82
+ export default function CheckoutLayout({ children }: { children: React.ReactNode }) {
83
+ return (
84
+ <>
85
+ <GlobalHeader
86
+ variant="checkout"
87
+ assets={{
88
+ logo: '/images/logo.svg',
89
+ miniLogo: '/images/mini-logo.svg',
90
+ }}
91
+ zoneContext="checkout"
92
+ />
93
+ {children}
94
+ </>
95
+ );
96
+ }
97
+ ```
@@ -0,0 +1,7 @@
1
+ import type { CategoryTreeItem } from '../types';
2
+ /**
3
+ * Active category tree from timmbr-core (public, cached). Returns an empty array when
4
+ * core is unreachable or misconfigured.
5
+ */
6
+ export declare function getCategoryTree(depth?: number): Promise<CategoryTreeItem[]>;
7
+ //# sourceMappingURL=categories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../src/api/categories.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAKjD;;;GAGG;AACH,wBAAsB,eAAe,CACnC,KAAK,GAAE,MAAsB,GAC5B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsB7B"}
@@ -0,0 +1,28 @@
1
+ import { safeApiCall } from '@timmbr/utils';
2
+ import { FALLBACK_CATEGORY_TREE } from '../fallbacks/categories.js';
3
+ import { getCoreServerClient } from './core-client.js';
4
+
5
+ const DEFAULT_DEPTH = 3;
6
+ async function getCategoryTree(depth = DEFAULT_DEPTH) {
7
+ const core = getCoreServerClient();
8
+ if (!core) {
9
+ console.warn(
10
+ "[CMS] CORE_API_URL (or NEXT_PUBLIC_API_URL) is not set; using empty category tree."
11
+ );
12
+ return FALLBACK_CATEGORY_TREE;
13
+ }
14
+ return safeApiCall(
15
+ () => core.get("/categories/tree", {
16
+ params: { depth },
17
+ skipAuth: true,
18
+ next: {
19
+ revalidate: process.env.NODE_ENV === "development" ? 0 : 600,
20
+ tags: ["categories", "storefront-category-tree"]
21
+ }
22
+ }),
23
+ FALLBACK_CATEGORY_TREE,
24
+ "[CMS:getCategoryTree]"
25
+ );
26
+ }
27
+
28
+ export { getCategoryTree };
@@ -0,0 +1,8 @@
1
+ import { type ApiClient } from '@timmbr/utils';
2
+ /** timmbr-core host reachable from the zone server (no trailing slash). */
3
+ export declare function resolveCoreBaseUrl(): string | undefined;
4
+ /** Server-side timmbr-core client (`/api/v1`, envelope unwrap). */
5
+ export declare function getCoreServerClient(): ApiClient | null;
6
+ /** Test helper: drop cached client between cases. */
7
+ export declare function __resetCoreServerClientForTests(): void;
8
+ //# sourceMappingURL=core-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core-client.d.ts","sourceRoot":"","sources":["../../src/api/core-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAEhE,2EAA2E;AAC3E,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAQvD;AAaD,mEAAmE;AACnE,wBAAgB,mBAAmB,IAAI,SAAS,GAAG,IAAI,CActD;AAED,qDAAqD;AACrD,wBAAgB,+BAA+B,SAG9C"}
@@ -0,0 +1,30 @@
1
+ import { createApiClient } from '@timmbr/utils';
2
+
3
+ function resolveCoreBaseUrl() {
4
+ const raw = process.env.CORE_API_URL || process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === "development" ? "http://localhost:8000" : void 0);
5
+ return raw?.replace(/\/+$/, "");
6
+ }
7
+ function coreDefaultHeaders() {
8
+ const key = process.env.CORE_INTERNAL_KEY;
9
+ if (key && key.length >= 32) {
10
+ return { "x-internal-key": key };
11
+ }
12
+ return void 0;
13
+ }
14
+ let client = null;
15
+ let clientBaseUrl;
16
+ function getCoreServerClient() {
17
+ const baseUrl = resolveCoreBaseUrl();
18
+ if (!baseUrl) return null;
19
+ if (!client || clientBaseUrl !== baseUrl) {
20
+ clientBaseUrl = baseUrl;
21
+ client = createApiClient({
22
+ baseUrl,
23
+ apiPrefix: "/api/v1",
24
+ defaultHeaders: coreDefaultHeaders()
25
+ });
26
+ }
27
+ return client;
28
+ }
29
+
30
+ export { getCoreServerClient, resolveCoreBaseUrl };
@@ -0,0 +1,11 @@
1
+ import type { StorefrontFooterAssets, StorefrontSocialLink, StorefrontSocialProp, StorefrontZoneContext } from '../types';
2
+ export interface GlobalFooterProps {
3
+ assets: StorefrontFooterAssets;
4
+ zoneContext?: StorefrontZoneContext;
5
+ /** Custom social media configuration, link array, or false to hide. Falls back to chromeCopy defaults. */
6
+ social?: StorefrontSocialProp;
7
+ /** Shorthand array of social links. */
8
+ socialLinks?: StorefrontSocialLink[];
9
+ }
10
+ export declare function GlobalFooter({ assets, zoneContext, social, socialLinks, }: GlobalFooterProps): Promise<import("react").JSX.Element>;
11
+ //# sourceMappingURL=GlobalFooter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GlobalFooter.d.ts","sourceRoot":"","sources":["../../src/components/GlobalFooter.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,0GAA0G;IAC1G,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,uCAAuC;IACvC,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACtC;AAED,wBAAsB,YAAY,CAAC,EACjC,MAAM,EACN,WAAqB,EACrB,MAAM,EACN,WAAW,GACZ,EAAE,iBAAiB,wCAkCnB"}
@@ -0,0 +1,44 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { getCategoryTree } from '../api/categories.js';
3
+ import { toNavTree, toFooterLinks, footerItemCrossZone } from '../mappers/nav.js';
4
+ import { chromeCopy } from '../copy/chrome.js';
5
+ import { StorefrontFooter } from './StorefrontFooter.js';
6
+
7
+ async function GlobalFooter({
8
+ assets,
9
+ zoneContext = "shell",
10
+ social,
11
+ socialLinks
12
+ }) {
13
+ const categories = toNavTree(await getCategoryTree());
14
+ const shopLinks = toFooterLinks(categories);
15
+ const footerZone = zoneContext === "catalog" ? "catalog" : "default";
16
+ const sections = [
17
+ ...shopLinks.length ? [{ id: "shop", title: chromeCopy.footer.shopTitle, items: shopLinks }] : [],
18
+ ...chromeCopy.footer.sections
19
+ ];
20
+ const navigation = {
21
+ sections: sections.map((section) => ({
22
+ id: section.id,
23
+ title: section.title,
24
+ items: section.items.map((item) => ({
25
+ id: item.id,
26
+ label: item.label,
27
+ href: item.href,
28
+ crossZone: footerItemCrossZone(item.href, footerZone)
29
+ }))
30
+ }))
31
+ };
32
+ return /* @__PURE__ */ jsx(
33
+ StorefrontFooter,
34
+ {
35
+ assets,
36
+ navigation,
37
+ zoneContext,
38
+ social,
39
+ socialLinks
40
+ }
41
+ );
42
+ }
43
+
44
+ export { GlobalFooter };
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ import type { StorefrontChromeAssets, StorefrontZoneContext } from '../types';
3
+ export interface GlobalHeaderProps {
4
+ variant?: 'store' | 'checkout';
5
+ assets: StorefrontChromeAssets;
6
+ zoneContext?: StorefrontZoneContext;
7
+ /** Shell-only: sale banner or other chrome below the sticky bar. */
8
+ belowNav?: React.ReactNode;
9
+ navHeaderClassName?: string;
10
+ useStickyShell?: boolean;
11
+ }
12
+ export declare function GlobalHeader({ variant, assets, zoneContext, belowNav, navHeaderClassName, useStickyShell, }: GlobalHeaderProps): Promise<React.JSX.Element>;
13
+ //# sourceMappingURL=GlobalHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GlobalHeader.d.ts","sourceRoot":"","sources":["../../src/components/GlobalHeader.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC/B,MAAM,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAsB,YAAY,CAAC,EACjC,OAAiB,EACjB,MAAM,EACN,WAAqB,EACrB,QAAQ,EACR,kBAAkB,EAClB,cAAsB,GACvB,EAAE,iBAAiB,8BAenB"}
@@ -0,0 +1,29 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { getCategoryTree } from '../api/categories.js';
3
+ import { toNavTree } from '../mappers/nav.js';
4
+ import { StorefrontHeader } from './StorefrontHeader.js';
5
+
6
+ async function GlobalHeader({
7
+ variant = "store",
8
+ assets,
9
+ zoneContext = "shell",
10
+ belowNav,
11
+ navHeaderClassName,
12
+ useStickyShell = false
13
+ }) {
14
+ const categories = variant === "checkout" ? [] : toNavTree(await getCategoryTree());
15
+ return /* @__PURE__ */ jsx(
16
+ StorefrontHeader,
17
+ {
18
+ variant,
19
+ assets,
20
+ categories,
21
+ zoneContext: variant === "checkout" ? "checkout" : zoneContext,
22
+ belowNav,
23
+ navHeaderClassName,
24
+ useStickyShell
25
+ }
26
+ );
27
+ }
28
+
29
+ export { GlobalHeader };
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import type { NavFooterNavigationConfig } from '@timmbr/ui';
3
+ import type { StorefrontFooterAssets, StorefrontSocialLink, StorefrontSocialProp, StorefrontZoneContext } from '../types';
4
+ export interface StorefrontFooterProps {
5
+ assets: StorefrontFooterAssets;
6
+ navigation: NavFooterNavigationConfig;
7
+ zoneContext: StorefrontZoneContext;
8
+ /** Custom social media configuration, link array, or false to hide. Falls back to chromeCopy defaults. */
9
+ social?: StorefrontSocialProp;
10
+ /** Shorthand array of social links. */
11
+ socialLinks?: StorefrontSocialLink[];
12
+ }
13
+ export declare function StorefrontFooter({ assets, navigation, zoneContext, social, socialLinks, }: StorefrontFooterProps): React.JSX.Element;
14
+ //# sourceMappingURL=StorefrontFooter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StorefrontFooter.d.ts","sourceRoot":"","sources":["../../src/components/StorefrontFooter.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,OAAO,KAAK,EAAE,yBAAyB,EAAuB,MAAM,YAAY,CAAC;AAWjF,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,UAAU,CAAC;AA4BlB,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,sBAAsB,CAAC;IAC/B,UAAU,EAAE,yBAAyB,CAAC;IACtC,WAAW,EAAE,qBAAqB,CAAC;IACnC,0GAA0G;IAC1G,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,uCAAuC;IACvC,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACtC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,UAAU,EACV,WAAW,EACX,MAAM,EACN,WAAW,GACZ,EAAE,qBAAqB,qBAwEvB"}
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import * as React from 'react';
4
+ import Image from 'next/image';
5
+ import Link from 'next/link';
6
+ import { NavFooter } from '@timmbr/ui';
7
+ import { AppleIcon, GoogleIcon, GitHubIcon, TwitterXIcon, LinkedInIcon, FacebookIcon, InstagramIcon } from '@timmbr/icons';
8
+ import { chromeCopy } from '../copy/chrome.js';
9
+
10
+ const SOCIAL_ICON_PROPS = {
11
+ width: 24,
12
+ height: 24,
13
+ className: "shrink-0"
14
+ };
15
+ const SOCIAL_ICONS = {
16
+ instagram: /* @__PURE__ */ jsx(InstagramIcon, { ...SOCIAL_ICON_PROPS }),
17
+ facebook: /* @__PURE__ */ jsx(FacebookIcon, { ...SOCIAL_ICON_PROPS }),
18
+ linkedin: /* @__PURE__ */ jsx(LinkedInIcon, { ...SOCIAL_ICON_PROPS }),
19
+ twitter: /* @__PURE__ */ jsx(TwitterXIcon, { ...SOCIAL_ICON_PROPS }),
20
+ x: /* @__PURE__ */ jsx(TwitterXIcon, { ...SOCIAL_ICON_PROPS }),
21
+ github: /* @__PURE__ */ jsx(GitHubIcon, { ...SOCIAL_ICON_PROPS }),
22
+ google: /* @__PURE__ */ jsx(GoogleIcon, { ...SOCIAL_ICON_PROPS }),
23
+ apple: /* @__PURE__ */ jsx(AppleIcon, { ...SOCIAL_ICON_PROPS })
24
+ };
25
+ function resolveSocialLinks(links) {
26
+ return links.map((link) => ({
27
+ ...link,
28
+ icon: link.icon ?? SOCIAL_ICONS[link.name.toLowerCase()]
29
+ }));
30
+ }
31
+ function StorefrontFooter({
32
+ assets,
33
+ navigation,
34
+ zoneContext,
35
+ social,
36
+ socialLinks
37
+ }) {
38
+ const useCatalogLinks = zoneContext === "catalog";
39
+ const resolvedSocial = React.useMemo(() => {
40
+ if (social === false) {
41
+ return { show: false, links: [] };
42
+ }
43
+ if (Array.isArray(social)) {
44
+ return {
45
+ title: chromeCopy.footer.socialTitle,
46
+ links: resolveSocialLinks(social)
47
+ };
48
+ }
49
+ if (social && typeof social === "object") {
50
+ return {
51
+ title: social.title ?? chromeCopy.footer.socialTitle,
52
+ show: social.show,
53
+ links: resolveSocialLinks(social.links ?? [])
54
+ };
55
+ }
56
+ if (socialLinks) {
57
+ return {
58
+ title: chromeCopy.footer.socialTitle,
59
+ links: resolveSocialLinks(socialLinks)
60
+ };
61
+ }
62
+ return {
63
+ title: chromeCopy.footer.socialTitle,
64
+ links: resolveSocialLinks(chromeCopy.footer.socialLinks)
65
+ };
66
+ }, [social, socialLinks]);
67
+ return /* @__PURE__ */ jsx(
68
+ NavFooter,
69
+ {
70
+ containerMaxWidth: "2xl",
71
+ linkComponent: useCatalogLinks ? Link : void 0,
72
+ branding: {
73
+ logo: /* @__PURE__ */ jsx(
74
+ Image,
75
+ {
76
+ src: assets.logo,
77
+ alt: chromeCopy.branding.logoAlt,
78
+ width: 140,
79
+ height: 34,
80
+ className: "h-8 w-auto object-contain"
81
+ }
82
+ ),
83
+ description: chromeCopy.footer.tagline,
84
+ href: "/",
85
+ alt: chromeCopy.branding.logoAlt,
86
+ ...useCatalogLinks ? { zone: "shell" } : {}
87
+ },
88
+ navigation,
89
+ social: resolvedSocial,
90
+ office: {
91
+ title: chromeCopy.footer.office.title,
92
+ address: chromeCopy.footer.office.address
93
+ },
94
+ legal: {
95
+ copyright: chromeCopy.footer.copyright,
96
+ links: chromeCopy.footer.legalLinks.map((link) => ({
97
+ id: link.id,
98
+ label: link.label,
99
+ href: link.href,
100
+ crossZone: useCatalogLinks
101
+ }))
102
+ }
103
+ }
104
+ );
105
+ }
106
+
107
+ export { StorefrontFooter };
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import type { CategoryNavNode, StorefrontChromeAssets, StorefrontZoneContext } from '../types';
3
+ export interface StorefrontHeaderProps {
4
+ variant: 'store' | 'checkout';
5
+ assets: StorefrontChromeAssets;
6
+ categories: CategoryNavNode[];
7
+ zoneContext: StorefrontZoneContext;
8
+ /** Shell: outer sticky wrapper and optional banner below the bar. */
9
+ useStickyShell?: boolean;
10
+ navHeaderClassName?: string;
11
+ belowNav?: React.ReactNode;
12
+ }
13
+ export declare function StorefrontHeader({ variant, assets, categories, zoneContext, useStickyShell, navHeaderClassName, belowNav, }: StorefrontHeaderProps): React.JSX.Element;
14
+ //# sourceMappingURL=StorefrontHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StorefrontHeader.d.ts","sourceRoot":"","sources":["../../src/components/StorefrontHeader.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAS/B,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACtB,MAAM,UAAU,CAAC;AAoBlB,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,GAAG,UAAU,CAAC;IAC9B,MAAM,EAAE,sBAAsB,CAAC;IAC/B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,WAAW,EAAE,qBAAqB,CAAC;IACnC,qEAAqE;IACrE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,MAAM,EACN,UAAU,EACV,WAAW,EACX,cAAsB,EACtB,kBAAkB,EAClB,QAAQ,GACT,EAAE,qBAAqB,qBA6HvB"}
@@ -0,0 +1,136 @@
1
+ "use client";
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import Image from 'next/image';
4
+ import Link from 'next/link';
5
+ import { toNavMenuItems, NavHeader, Inline, Icon, Text } from '@timmbr/ui';
6
+ import { useProfileAction, useCartCount } from '@timmbr/auth';
7
+ import { Lock, User, Package } from '@timmbr/icons';
8
+ import { chromeCopy } from '../copy/chrome.js';
9
+ import { useStorefrontSearch } from '../hooks/useStorefrontSearch.js';
10
+ import { toCategoryMenus } from '../mappers/nav.js';
11
+
12
+ const MENU_ICONS = {
13
+ orders: /* @__PURE__ */ jsx(Icon, { size: "sm", children: /* @__PURE__ */ jsx(Package, {}) }),
14
+ account: /* @__PURE__ */ jsx(Icon, { size: "sm", children: /* @__PURE__ */ jsx(User, {}) })
15
+ };
16
+ const PROFILE_MENU = chromeCopy.header.actions.account.menu.map((item) => ({
17
+ ...item,
18
+ icon: MENU_ICONS[item.id]
19
+ }));
20
+ function StorefrontHeader({
21
+ variant,
22
+ assets,
23
+ categories,
24
+ zoneContext,
25
+ useStickyShell = false,
26
+ navHeaderClassName,
27
+ belowNav
28
+ }) {
29
+ const { profileAction, onAuthTrigger } = useProfileAction({
30
+ accountHref: chromeCopy.header.actions.account.href,
31
+ menuItems: PROFILE_MENU
32
+ });
33
+ const search = useStorefrontSearch();
34
+ const cartCount = useCartCount() ?? 0;
35
+ const navItems = variant === "store" ? toNavMenuItems(toCategoryMenus(categories, chromeCopy.header.categoryNav), {
36
+ ...zoneContext === "catalog" ? { linkComponent: Link } : { crossZone: true }
37
+ }) : [];
38
+ const branding = {
39
+ logo: /* @__PURE__ */ jsx(
40
+ Image,
41
+ {
42
+ src: assets.logo,
43
+ alt: chromeCopy.branding.logoAlt,
44
+ width: 130,
45
+ height: 32,
46
+ priority: true,
47
+ className: "h-8 w-auto object-contain"
48
+ }
49
+ ),
50
+ miniLogo: /* @__PURE__ */ jsx(
51
+ Image,
52
+ {
53
+ src: assets.miniLogo,
54
+ alt: chromeCopy.branding.miniLogoAlt,
55
+ width: 32,
56
+ height: 32,
57
+ priority: true,
58
+ className: variant === "checkout" ? "h-8 w-8 object-contain" : "h-8 w-auto object-contain"
59
+ }
60
+ ),
61
+ href: "/",
62
+ alt: chromeCopy.branding.logoAlt,
63
+ ...zoneContext === "catalog" ? { zone: "shell" } : {}
64
+ };
65
+ const header = variant === "checkout" ? /* @__PURE__ */ jsx(
66
+ NavHeader,
67
+ {
68
+ sticky: !useStickyShell,
69
+ authMode: "modal",
70
+ onAuthTrigger,
71
+ showOfferBanner: false,
72
+ containerMaxWidth: "2xl",
73
+ branding,
74
+ search: false,
75
+ navigation: { show: false },
76
+ mobile: { tabSlider: false },
77
+ actions: {
78
+ items: [
79
+ {
80
+ id: "secure",
81
+ label: chromeCopy.header.secure,
82
+ icon: /* @__PURE__ */ jsx(Lock, {}),
83
+ priority: "high",
84
+ render: () => /* @__PURE__ */ jsxs(Inline, { gap: 2, align: "center", className: "shrink-0 px-2", children: [
85
+ /* @__PURE__ */ jsx(Icon, { size: "sm", children: /* @__PURE__ */ jsx(Lock, {}) }),
86
+ /* @__PURE__ */ jsx(Text, { variant: "body-3", weight: "medium", foreground: "muted", children: chromeCopy.header.secure })
87
+ ] })
88
+ }
89
+ ],
90
+ profile: profileAction,
91
+ wishlist: false,
92
+ cart: false
93
+ },
94
+ className: navHeaderClassName
95
+ }
96
+ ) : /* @__PURE__ */ jsx(
97
+ NavHeader,
98
+ {
99
+ sticky: !useStickyShell,
100
+ authMode: "modal",
101
+ onAuthTrigger,
102
+ showOfferBanner: false,
103
+ containerMaxWidth: "2xl",
104
+ linkComponent: zoneContext === "catalog" ? Link : void 0,
105
+ branding,
106
+ navigation: { items: navItems },
107
+ search,
108
+ actions: {
109
+ profile: profileAction,
110
+ wishlist: {
111
+ label: chromeCopy.header.actions.wishlist.label,
112
+ href: chromeCopy.header.actions.wishlist.href,
113
+ ariaLabel: chromeCopy.header.actions.wishlist.ariaLabel,
114
+ crossZone: true
115
+ },
116
+ cart: {
117
+ label: chromeCopy.header.actions.cart.label,
118
+ href: chromeCopy.header.actions.cart.href,
119
+ ariaLabel: chromeCopy.header.actions.cart.ariaLabel,
120
+ count: cartCount,
121
+ ...zoneContext === "shell" ? {} : { crossZone: true }
122
+ }
123
+ },
124
+ className: navHeaderClassName
125
+ }
126
+ );
127
+ if (!useStickyShell) {
128
+ return header;
129
+ }
130
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
131
+ /* @__PURE__ */ jsx("div", { className: "sticky top-0 z-40", children: header }),
132
+ belowNav
133
+ ] });
134
+ }
135
+
136
+ export { StorefrontHeader };
@@ -0,0 +1,126 @@
1
+ /** Shared storefront chrome copy (header, footer, branding). */
2
+ export declare const chromeCopy: {
3
+ readonly branding: {
4
+ readonly logoAlt: "timmbr - Solid Wood Furniture";
5
+ readonly miniLogoAlt: "timmbr leaf icon";
6
+ };
7
+ readonly header: {
8
+ readonly search: {
9
+ readonly placeholder: "Search solid wood furniture, collections, artisanal pieces...";
10
+ readonly ariaLabel: "Search products";
11
+ readonly recentTitle: "Recent searches";
12
+ readonly clearRecent: "Clear";
13
+ readonly removeRecent: (query: string) => string;
14
+ readonly resultsTitle: "Products";
15
+ readonly noResults: "No products match";
16
+ readonly seeAll: (query: string) => string;
17
+ };
18
+ readonly categoryNav: {
19
+ readonly viewAll: (name: string) => string;
20
+ readonly viewMore: "View all";
21
+ };
22
+ readonly actions: {
23
+ readonly account: {
24
+ readonly href: "/account";
25
+ readonly menu: readonly [{
26
+ readonly id: "orders";
27
+ readonly label: "My orders";
28
+ readonly href: "/account/orders";
29
+ }, {
30
+ readonly id: "account";
31
+ readonly label: "Your account";
32
+ readonly href: "/account/security";
33
+ }];
34
+ };
35
+ readonly wishlist: {
36
+ readonly label: "WISH LIST";
37
+ readonly ariaLabel: "Wishlist";
38
+ readonly href: "/account/wishlist";
39
+ };
40
+ readonly cart: {
41
+ readonly label: "CART";
42
+ readonly ariaLabel: "Shopping Cart";
43
+ readonly href: "/checkout";
44
+ };
45
+ };
46
+ readonly secure: "Secure checkout";
47
+ };
48
+ readonly footer: {
49
+ readonly tagline: "Handcrafted solid wood furniture built with precision, finished slowly, and made to last generations.";
50
+ readonly office: {
51
+ readonly title: "Registered Office:";
52
+ readonly address: "Timmbr Furnitures, SABN Kallappa Layout, Ashwini Extension, Chintamani, Karnataka 563125";
53
+ };
54
+ readonly shopTitle: "Shop";
55
+ readonly sections: readonly [{
56
+ readonly id: "craft";
57
+ readonly title: "Craft";
58
+ readonly items: readonly [{
59
+ readonly id: "materials";
60
+ readonly label: "Our materials";
61
+ readonly href: "/craft/materials";
62
+ }, {
63
+ readonly id: "workshop";
64
+ readonly label: "The workshop";
65
+ readonly href: "/craft/workshop";
66
+ }, {
67
+ readonly id: "sustainability";
68
+ readonly label: "Sustainability";
69
+ readonly href: "/craft/sustainability";
70
+ }, {
71
+ readonly id: "care";
72
+ readonly label: "Care guide";
73
+ readonly href: "/craft/care-guide";
74
+ }];
75
+ }, {
76
+ readonly id: "help";
77
+ readonly title: "Help";
78
+ readonly items: readonly [{
79
+ readonly id: "contact";
80
+ readonly label: "Contact us";
81
+ readonly href: "/help/contact";
82
+ }, {
83
+ readonly id: "shipping";
84
+ readonly label: "Shipping & delivery";
85
+ readonly href: "/help/shipping";
86
+ }, {
87
+ readonly id: "warranty";
88
+ readonly label: "Warranty & returns";
89
+ readonly href: "/help/warranty";
90
+ }, {
91
+ readonly id: "trade";
92
+ readonly label: "Trade & commercial";
93
+ readonly href: "/help/trade";
94
+ }];
95
+ }];
96
+ readonly socialTitle: "Follow Us";
97
+ readonly socialLinks: readonly [{
98
+ readonly name: "instagram";
99
+ readonly href: "https://instagram.com";
100
+ }, {
101
+ readonly name: "facebook";
102
+ readonly href: "https://facebook.com";
103
+ }, {
104
+ readonly name: "linkedin";
105
+ readonly href: "https://linkedin.com";
106
+ }, {
107
+ readonly name: "twitter";
108
+ readonly href: "https://x.com";
109
+ }];
110
+ readonly copyright: "© 2026 Timmbr Furniture Co. All rights reserved.";
111
+ readonly legalLinks: readonly [{
112
+ readonly id: "privacy";
113
+ readonly label: "Privacy policy";
114
+ readonly href: "/legal/privacy";
115
+ }, {
116
+ readonly id: "terms";
117
+ readonly label: "Terms of service";
118
+ readonly href: "/legal/terms";
119
+ }, {
120
+ readonly id: "cookies";
121
+ readonly label: "Cookie preferences";
122
+ readonly href: "/legal/cookies";
123
+ }];
124
+ };
125
+ };
126
+ //# sourceMappingURL=chrome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chrome.d.ts","sourceRoot":"","sources":["../../src/copy/chrome.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,eAAO,MAAM,UAAU;;;;;;;;;;;2CAYK,MAAM;;;qCAGZ,MAAM;;;qCAGN,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiFlB,CAAC"}
@@ -0,0 +1,99 @@
1
+ const chromeCopy = {
2
+ branding: {
3
+ logoAlt: "timmbr - Solid Wood Furniture",
4
+ miniLogoAlt: "timmbr leaf icon"
5
+ },
6
+ header: {
7
+ search: {
8
+ placeholder: "Search solid wood furniture, collections, artisanal pieces...",
9
+ ariaLabel: "Search products",
10
+ recentTitle: "Recent searches",
11
+ clearRecent: "Clear",
12
+ removeRecent: (query) => `Remove “${query}” from recent searches`,
13
+ resultsTitle: "Products",
14
+ noResults: "No products match",
15
+ seeAll: (query) => `See all results for “${query}”`
16
+ },
17
+ categoryNav: {
18
+ viewAll: (name) => `All ${name}`,
19
+ viewMore: "View all"
20
+ },
21
+ actions: {
22
+ account: {
23
+ href: "/account",
24
+ menu: [
25
+ { id: "orders", label: "My orders", href: "/account/orders" },
26
+ { id: "account", label: "Your account", href: "/account/security" }
27
+ ]
28
+ },
29
+ wishlist: {
30
+ label: "WISH LIST",
31
+ ariaLabel: "Wishlist",
32
+ href: "/account/wishlist"
33
+ },
34
+ cart: {
35
+ label: "CART",
36
+ ariaLabel: "Shopping Cart",
37
+ href: "/checkout"
38
+ }
39
+ },
40
+ secure: "Secure checkout"
41
+ },
42
+ footer: {
43
+ tagline: "Handcrafted solid wood furniture built with precision, finished slowly, and made to last generations.",
44
+ office: {
45
+ title: "Registered Office:",
46
+ address: "Timmbr Furnitures, SABN Kallappa Layout, Ashwini Extension, Chintamani, Karnataka 563125"
47
+ },
48
+ shopTitle: "Shop",
49
+ sections: [
50
+ {
51
+ id: "craft",
52
+ title: "Craft",
53
+ items: [
54
+ { id: "materials", label: "Our materials", href: "/craft/materials" },
55
+ { id: "workshop", label: "The workshop", href: "/craft/workshop" },
56
+ {
57
+ id: "sustainability",
58
+ label: "Sustainability",
59
+ href: "/craft/sustainability"
60
+ },
61
+ { id: "care", label: "Care guide", href: "/craft/care-guide" }
62
+ ]
63
+ },
64
+ {
65
+ id: "help",
66
+ title: "Help",
67
+ items: [
68
+ { id: "contact", label: "Contact us", href: "/help/contact" },
69
+ {
70
+ id: "shipping",
71
+ label: "Shipping & delivery",
72
+ href: "/help/shipping"
73
+ },
74
+ {
75
+ id: "warranty",
76
+ label: "Warranty & returns",
77
+ href: "/help/warranty"
78
+ },
79
+ { id: "trade", label: "Trade & commercial", href: "/help/trade" }
80
+ ]
81
+ }
82
+ ],
83
+ socialTitle: "Follow Us",
84
+ socialLinks: [
85
+ { name: "instagram", href: "https://instagram.com" },
86
+ { name: "facebook", href: "https://facebook.com" },
87
+ { name: "linkedin", href: "https://linkedin.com" },
88
+ { name: "twitter", href: "https://x.com" }
89
+ ],
90
+ copyright: "© 2026 Timmbr Furniture Co. All rights reserved.",
91
+ legalLinks: [
92
+ { id: "privacy", label: "Privacy policy", href: "/legal/privacy" },
93
+ { id: "terms", label: "Terms of service", href: "/legal/terms" },
94
+ { id: "cookies", label: "Cookie preferences", href: "/legal/cookies" }
95
+ ]
96
+ }
97
+ };
98
+
99
+ export { chromeCopy };
@@ -0,0 +1,4 @@
1
+ import type { CategoryTreeItem } from '../types';
2
+ /** Empty tree: chrome still renders logo, search, and static footer sections. */
3
+ export declare const FALLBACK_CATEGORY_TREE: CategoryTreeItem[];
4
+ //# sourceMappingURL=categories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../src/fallbacks/categories.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,iFAAiF;AACjF,eAAO,MAAM,sBAAsB,EAAE,gBAAgB,EAAO,CAAC"}
@@ -0,0 +1,3 @@
1
+ const FALLBACK_CATEGORY_TREE = [];
2
+
3
+ export { FALLBACK_CATEGORY_TREE };
@@ -0,0 +1,19 @@
1
+ import type { NavSearchConfig, NavSearchResult } from '@timmbr/ui';
2
+ export declare const RECENT_SEARCHES_KEY = "timmbr:recent-searches:v1";
3
+ interface SuggestedProduct {
4
+ id: string;
5
+ slug: string;
6
+ title: string;
7
+ brandName: string | null;
8
+ price: number;
9
+ currency: string;
10
+ coverImage: {
11
+ url: string;
12
+ alt: string | null;
13
+ } | null;
14
+ }
15
+ export declare const searchHref: (query: string) => string;
16
+ export declare function toSearchResult(product: SuggestedProduct): NavSearchResult;
17
+ export declare function useStorefrontSearch(): NavSearchConfig;
18
+ export {};
19
+ //# sourceMappingURL=useStorefrontSearch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStorefrontSearch.d.ts","sourceRoot":"","sources":["../../src/hooks/useStorefrontSearch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAInE,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAG/D,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACxD;AAED,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,WACU,CAAC;AAenD,wBAAgB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAWzE;AAED,wBAAgB,mBAAmB,IAAI,eAAe,CA2CrD"}
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { useState, useCallback, useMemo } from 'react';
3
+ import { useRecentSearches, useSearchSuggestions } from '@timmbr/hooks';
4
+ import { formatCurrency } from '@timmbr/utils';
5
+ import { chromeCopy } from '../copy/chrome.js';
6
+
7
+ const RECENT_SEARCHES_KEY = "timmbr:recent-searches:v1";
8
+ const SUGGEST_URL = "/products/api/search-suggest";
9
+ const searchHref = (query) => `/products?${new URLSearchParams({ q: query })}`;
10
+ async function fetchSuggestions(query, signal) {
11
+ const response = await fetch(
12
+ `${SUGGEST_URL}?${new URLSearchParams({ q: query })}`,
13
+ { signal }
14
+ );
15
+ if (!response.ok) return [];
16
+ const body = await response.json();
17
+ return body.products ?? [];
18
+ }
19
+ function toSearchResult(product) {
20
+ return {
21
+ id: product.id,
22
+ href: `/products/${product.slug}`,
23
+ crossZone: true,
24
+ title: product.title,
25
+ subtitle: product.brandName ?? void 0,
26
+ price: formatCurrency(product.price, product.currency),
27
+ imageUrl: product.coverImage?.url,
28
+ imageAlt: product.coverImage?.alt ?? ""
29
+ };
30
+ }
31
+ function useStorefrontSearch() {
32
+ const [value, setValue] = useState("");
33
+ const recent = useRecentSearches({ storageKey: RECENT_SEARCHES_KEY });
34
+ const suggestions = useSearchSuggestions(value, fetchSuggestions);
35
+ const { add } = recent;
36
+ const copy = chromeCopy.header.search;
37
+ const onSubmit = useCallback(
38
+ (query) => {
39
+ const q = query.trim();
40
+ if (!q) return;
41
+ add(q);
42
+ window.location.assign(searchHref(q));
43
+ },
44
+ [add]
45
+ );
46
+ const items = useMemo(
47
+ () => suggestions.items.map(toSearchResult),
48
+ [suggestions.items]
49
+ );
50
+ return {
51
+ show: true,
52
+ placeholder: copy.placeholder,
53
+ ariaLabel: copy.ariaLabel,
54
+ value,
55
+ onChange: setValue,
56
+ onSubmit,
57
+ recentSearches: recent.items,
58
+ recentSearchesTitle: copy.recentTitle,
59
+ onRecentSearchRemove: recent.remove,
60
+ onClearRecentSearches: recent.clear,
61
+ clearRecentSearchesLabel: copy.clearRecent,
62
+ removeRecentSearchLabel: copy.removeRecent,
63
+ results: {
64
+ items,
65
+ loading: suggestions.loading,
66
+ title: copy.resultsTitle,
67
+ emptyLabel: copy.noResults,
68
+ viewAllLabel: copy.seeAll
69
+ }
70
+ };
71
+ }
72
+
73
+ export { RECENT_SEARCHES_KEY, searchHref, toSearchResult, useStorefrontSearch };
@@ -0,0 +1,15 @@
1
+ export { getCategoryTree } from './api/categories';
2
+ export { GlobalHeader } from './components/GlobalHeader';
3
+ export type { GlobalHeaderProps } from './components/GlobalHeader';
4
+ export { GlobalFooter } from './components/GlobalFooter';
5
+ export type { GlobalFooterProps } from './components/GlobalFooter';
6
+ export { StorefrontHeader } from './components/StorefrontHeader';
7
+ export type { StorefrontHeaderProps } from './components/StorefrontHeader';
8
+ export { StorefrontFooter } from './components/StorefrontFooter';
9
+ export type { StorefrontFooterProps } from './components/StorefrontFooter';
10
+ export { toNavTree, toCategoryMenus, toFooterLinks, isCatalogHref, footerItemCrossZone, NAV_ROOT_LIMIT, NAV_LINKS_PER_GROUP, categoryHref, toMenuGroups, toMenuTiles, } from './mappers/nav';
11
+ export type { CategoryNavCopy } from './mappers/nav';
12
+ export type { CategoryNavNode, CategoryTreeItem, StorefrontZoneContext, StorefrontChromeAssets, StorefrontFooterAssets, StorefrontSocialLink, StorefrontSocialConfig, StorefrontSocialProp, } from './types';
13
+ export { chromeCopy } from './copy/chrome';
14
+ export { RECENT_SEARCHES_KEY, useStorefrontSearch } from './hooks/useStorefrontSearch';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,YAAY,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,YAAY,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EACL,SAAS,EACT,eAAe,EACf,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EACV,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { getCategoryTree } from './api/categories.js';
2
+ export { GlobalHeader } from './components/GlobalHeader.js';
3
+ export { GlobalFooter } from './components/GlobalFooter.js';
4
+ export { StorefrontHeader } from './components/StorefrontHeader.js';
5
+ export { StorefrontFooter } from './components/StorefrontFooter.js';
6
+ export { NAV_LINKS_PER_GROUP, NAV_ROOT_LIMIT, categoryHref, footerItemCrossZone, isCatalogHref, toCategoryMenus, toFooterLinks, toMenuGroups, toMenuTiles, toNavTree } from './mappers/nav.js';
7
+ export { chromeCopy } from './copy/chrome.js';
8
+ export { RECENT_SEARCHES_KEY, useStorefrontSearch } from './hooks/useStorefrontSearch.js';
@@ -0,0 +1,25 @@
1
+ import type { NavMenuData, NavMenuGroup, NavMenuTile } from '@timmbr/ui';
2
+ import type { CategoryNavNode, CategoryTreeItem } from '../types';
3
+ /** Roots shown in the header and footer (no ranking yet: the first by name). */
4
+ export declare const NAV_ROOT_LIMIT = 5;
5
+ /** Subcategory links under one heading before a "View all" link takes over. */
6
+ export declare const NAV_LINKS_PER_GROUP = 8;
7
+ export interface CategoryNavCopy {
8
+ viewAll: (name: string) => string;
9
+ viewMore: string;
10
+ }
11
+ export declare const categoryHref: (slug: string) => string;
12
+ export declare function toNavTree(tree: CategoryTreeItem[]): CategoryNavNode[];
13
+ export declare function toMenuGroups(root: CategoryNavNode, copy: CategoryNavCopy): NavMenuGroup[];
14
+ export declare function toMenuTiles(root: CategoryNavNode, copy: CategoryNavCopy): NavMenuTile[];
15
+ export declare function toCategoryMenus(tree: CategoryNavNode[], copy: CategoryNavCopy): NavMenuData[];
16
+ export declare function toFooterLinks(tree: CategoryNavNode[]): {
17
+ id: string;
18
+ label: string;
19
+ href: string;
20
+ }[];
21
+ /** True when href targets the catalog zone (cross-zone from shell/account). */
22
+ export declare function isCatalogHref(href: string): boolean;
23
+ /** Footer nav item: crossZone when leaving the rendering zone. */
24
+ export declare function footerItemCrossZone(href: string, zone: 'catalog' | 'default'): boolean;
25
+ //# sourceMappingURL=nav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav.d.ts","sourceRoot":"","sources":["../../src/mappers/nav.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAElE,gFAAgF;AAChF,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,+EAA+E;AAC/E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,WAAwB,CAAC;AAUlE,wBAAgB,SAAS,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,eAAe,EAAE,CAQrE;AAED,wBAAgB,YAAY,CAC1B,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,eAAe,GACpB,YAAY,EAAE,CAiChB;AAED,wBAAgB,WAAW,CACzB,IAAI,EAAE,eAAe,EACrB,IAAI,EAAE,eAAe,GACpB,WAAW,EAAE,CASf;AAED,wBAAgB,eAAe,CAC7B,IAAI,EAAE,eAAe,EAAE,EACvB,IAAI,EAAE,eAAe,GACpB,WAAW,EAAE,CAOf;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,EAAE;;;;IAEpD;AAID,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,WAKzC;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,GAAG,SAAS,WAI5B"}
@@ -0,0 +1,80 @@
1
+ import { mapTree } from '@timmbr/utils';
2
+
3
+ const NAV_ROOT_LIMIT = 5;
4
+ const NAV_LINKS_PER_GROUP = 8;
5
+ const categoryHref = (slug) => `/category/${slug}`;
6
+ const navRoots = (tree) => tree.slice(0, NAV_ROOT_LIMIT);
7
+ const link = (node, label = node.name) => ({
8
+ id: node.id,
9
+ label,
10
+ href: categoryHref(node.slug)
11
+ });
12
+ function toNavTree(tree) {
13
+ return mapTree(navRoots(tree), (node, children) => ({
14
+ id: node.id,
15
+ name: node.name,
16
+ slug: node.slug,
17
+ logoUrl: node.logoUrl ?? null,
18
+ children
19
+ }));
20
+ }
21
+ function toMenuGroups(root, copy) {
22
+ const children = root.children ?? [];
23
+ const leaves = children.filter((child) => !child.children?.length);
24
+ const branches = children.filter((child) => child.children?.length);
25
+ const rootGroup = {
26
+ id: root.id,
27
+ title: root.name,
28
+ links: [
29
+ link(root, copy.viewAll(root.name)),
30
+ ...leaves.map((node) => link(node))
31
+ ]
32
+ };
33
+ const childGroups = branches.map((child) => {
34
+ const grandchildren = child.children ?? [];
35
+ return {
36
+ id: child.id,
37
+ title: child.name,
38
+ links: [
39
+ link(child, copy.viewAll(child.name)),
40
+ ...grandchildren.slice(0, NAV_LINKS_PER_GROUP).map((node) => link(node))
41
+ ],
42
+ more: grandchildren.length > NAV_LINKS_PER_GROUP ? { label: copy.viewMore, href: categoryHref(child.slug) } : void 0
43
+ };
44
+ });
45
+ return [rootGroup, ...childGroups];
46
+ }
47
+ function toMenuTiles(root, copy) {
48
+ const children = root.children ?? [];
49
+ return [
50
+ {
51
+ ...link(root, copy.viewAll(root.name)),
52
+ imageSrc: root.logoUrl ?? children.find((c) => c.logoUrl)?.logoUrl
53
+ },
54
+ ...children.map((child) => ({ ...link(child), imageSrc: child.logoUrl }))
55
+ ];
56
+ }
57
+ function toCategoryMenus(tree, copy) {
58
+ return navRoots(tree).map((root) => ({
59
+ id: root.id,
60
+ label: root.name,
61
+ groups: toMenuGroups(root, copy),
62
+ tiles: toMenuTiles(root, copy)
63
+ }));
64
+ }
65
+ function toFooterLinks(tree) {
66
+ return navRoots(tree).map((root) => link(root));
67
+ }
68
+ const CATALOG_PREFIXES = ["/products", "/category"];
69
+ function isCatalogHref(href) {
70
+ const path = href.split(/[?#]/)[0];
71
+ return CATALOG_PREFIXES.some(
72
+ (prefix) => path === prefix || path.startsWith(`${prefix}/`)
73
+ );
74
+ }
75
+ function footerItemCrossZone(href, zone) {
76
+ const catalog = isCatalogHref(href);
77
+ return zone === "catalog" ? !catalog : catalog;
78
+ }
79
+
80
+ export { NAV_LINKS_PER_GROUP, NAV_ROOT_LIMIT, categoryHref, footerItemCrossZone, isCatalogHref, toCategoryMenus, toFooterLinks, toMenuGroups, toMenuTiles, toNavTree };
@@ -0,0 +1,28 @@
1
+ import type { NavFooterSocialConfig, NavFooterSocialLink } from '@timmbr/ui';
2
+ export type StorefrontSocialLink = NavFooterSocialLink;
3
+ export type StorefrontSocialConfig = NavFooterSocialConfig;
4
+ export type StorefrontSocialProp = StorefrontSocialConfig | StorefrontSocialLink[] | false;
5
+ export interface CategoryTreeItem {
6
+ id: string;
7
+ name: string;
8
+ slug: string;
9
+ logoUrl?: string | null;
10
+ parentId?: string | null;
11
+ children?: CategoryTreeItem[];
12
+ }
13
+ export interface CategoryNavNode {
14
+ id: string;
15
+ name: string;
16
+ slug: string;
17
+ logoUrl?: string | null;
18
+ children?: CategoryNavNode[];
19
+ }
20
+ export type StorefrontZoneContext = 'shell' | 'catalog' | 'account' | 'checkout';
21
+ export interface StorefrontChromeAssets {
22
+ logo: string;
23
+ miniLogo: string;
24
+ }
25
+ export interface StorefrontFooterAssets {
26
+ logo: string;
27
+ }
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE7E,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AACvD,MAAM,MAAM,sBAAsB,GAAG,qBAAqB,CAAC;AAC3D,MAAM,MAAM,oBAAoB,GAC5B,sBAAsB,GACtB,oBAAoB,EAAE,GACtB,KAAK,CAAC;AAEV,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;CAC9B;AAED,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAEjF,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;CACd"}
package/package.json CHANGED
@@ -1,6 +1,73 @@
1
1
  {
2
2
  "name": "@timmbr/cms",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0",
4
+ "description": "Storefront chrome integration: category tree fetch, nav mapping, GlobalHeader and GlobalFooter for multi-zone layouts",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@timmbr/icons": "^1.7.0",
25
+ "@timmbr/utils": "^1.6.0"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18.0.0",
29
+ "react-dom": ">=18.0.0",
30
+ "next": ">=14.0.0",
31
+ "@timmbr/ui": "*",
32
+ "@timmbr/auth": "*",
33
+ "@timmbr/hooks": "*"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@timmbr/ui": {
37
+ "optional": false
38
+ },
39
+ "@timmbr/auth": {
40
+ "optional": false
41
+ },
42
+ "@timmbr/hooks": {
43
+ "optional": false
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@testing-library/jest-dom": "^6.6.3",
48
+ "@testing-library/react": "^16.2.0",
49
+ "@types/react": "^19.0.8",
50
+ "@types/react-dom": "^19.0.3",
51
+ "@vitejs/plugin-react": "^4.3.4",
52
+ "jsdom": "^26.0.0",
53
+ "next": "16.3.5",
54
+ "react": "^19.0.0",
55
+ "react-dom": "^19.0.0",
56
+ "rimraf": "^6.0.1",
57
+ "rollup-plugin-preserve-directives": "^0.4.0",
58
+ "typescript": "^5.7.3",
59
+ "vite": "^6.2.0",
60
+ "vitest": "^3.0.5",
61
+ "@timmbr/ui": "1.17.6",
62
+ "@timmbr/auth": "1.4.7",
63
+ "@timmbr/hooks": "1.3.0",
64
+ "@timmbr/tsconfig": "1.0.0"
65
+ },
66
+ "scripts": {
67
+ "build": "vite build && tsc --project tsconfig.build.json",
68
+ "dev": "vite build --watch",
69
+ "typecheck": "tsc --noEmit",
70
+ "test": "vitest run",
71
+ "clean": "rimraf dist"
72
+ }
6
73
  }