@168group/brand-family 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ Copyright (c) 2026 168 Group Inc. All rights reserved.
2
+
3
+ This package and its included brand assets are provided for authorized 168 Group
4
+ websites. No license is granted to reproduce, redistribute, modify, or use the
5
+ included trademarks or brand assets outside authorized 168 Group properties without
6
+ prior written permission from 168 Group Inc.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @168group/brand-family
2
+
3
+ The shared 168 Group portfolio rail for React and Next.js websites. It renders
4
+ server-visible brand links, an accessible pauseable desktop marquee, and a manually
5
+ swipeable touch experience with no Tailwind or animation-library dependency.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @168group/brand-family
11
+ ```
12
+
13
+ Import the stylesheet once in the host application's global entry:
14
+
15
+ ```tsx
16
+ import "@168group/brand-family/styles.css";
17
+ ```
18
+
19
+ Then place the component on the site's homepage, preferably immediately after the primary
20
+ hero or introductory section. This keeps the family visible without placing a large
21
+ marketing rail above the main purpose of every inner page:
22
+
23
+ ```tsx
24
+ import { BrandFamilyRail } from "@168group/brand-family";
25
+
26
+ export function HomePage() {
27
+ return (
28
+ <>
29
+ <Hero />
30
+ <BrandFamilyRail
31
+ currentBrandSlug="laopot"
32
+ openLinksInNewTab
33
+ />
34
+ <HomePageContent />
35
+ </>
36
+ );
37
+ }
38
+ ```
39
+
40
+ Use the slug exported in `brandFamily` for `currentBrandSlug`. The current site stays
41
+ visible in the family but is rendered without a self-link.
42
+
43
+ `openLinksInNewTab` is optional and defaults to `false`. When enabled, linked cards use
44
+ `target="_blank"`, `rel="noopener"`, an external-link arrow, and an accessible new-tab
45
+ announcement. Opening a new tab can help visitors keep their place on the current brand
46
+ site, but does not provide additional search-ranking value.
47
+
48
+ ## Customization
49
+
50
+ Override the package CSS variables on a wrapper or the component itself:
51
+
52
+ ```tsx
53
+ <BrandFamilyRail
54
+ currentBrandSlug="168-asian-mart"
55
+ style={{
56
+ "--bfr-crimson": "#b7182e",
57
+ "--bfr-gold": "#d4af37",
58
+ } as React.CSSProperties}
59
+ />
60
+ ```
61
+
62
+ The `onBrandNavigate` callback can forward click events to the host site's analytics.
63
+ Navigation remains native and continues even if that callback fails.
64
+
65
+ ## Preparing and replacing logos
66
+
67
+ When retained, original supplied artwork lives in `assets/logos/logos/` and is
68
+ intentionally excluded from the published package. The preparation script safely skips
69
+ an original that is no longer present. The web-ready assets in `assets/logos/` use a consistent
70
+ transparent `600 × 300` canvas—enough for crisp high-density card rendering without
71
+ shipping oversized artwork. To regenerate the supplied marks with Pillow and NumPy:
72
+
73
+ ```bash
74
+ python3 scripts/prepare-logos.py
75
+ ```
76
+
77
+ The script preserves every original. It removes the edge-connected white matte from
78
+ 168 Asian Mart, removes and decontaminates the black JPEG matte from Asian Street Food,
79
+ and—when the full ASF original is present—favors its primary bowl mark over the too-small
80
+ card tagline. It trims transparent PNGs and presents the flattened Ebisu campaign graphic
81
+ as a rounded tile. Ebisu needs a
82
+ layered/vector original before its pink rays can be removed without damaging the mascot
83
+ and lettering. The uploaded `icon.png` is retained as the source for the normalized
84
+ `168-nail-supply.png` asset. `fuji.png` is the source for `fuji-buffet.png`, and the
85
+ white matte is removed from `rdf_logo.webp` when producing
86
+ `ricky-dong-foundation.png`. A final lower-wordmark pass removes enclosed white matte
87
+ remnants from the Asian Mart and foundation lettering without touching the white artwork
88
+ inside their upper badges.
89
+
90
+ The published web-ready logo files live in `assets/logos/`. Replace a file without
91
+ changing its name, preserve a wide `2:1` canvas when practical, and run:
92
+
93
+ ```bash
94
+ npm run check --workspace=@168group/brand-family
95
+ ```
96
+
97
+ Prefer clean vector originals. If a brand has only a raster logo, convert it to an
98
+ optimized transparent WebP or PNG and update the matching import in `src/brands.ts`.
99
+ Keep every individual asset small enough for the complete logo set to remain lightweight.
100
+
101
+ ## Release
102
+
103
+ 1. Update the package version according to semantic versioning.
104
+ 2. Run the package check and the umbrella site's lint/build.
105
+ 3. Inspect the packed contents with `npm pack --dry-run --workspace=@168group/brand-family`.
106
+ 4. Publish to public npm. The package pins both `publishConfig.access = "public"` and the
107
+ official npm registry so a developer's user-level scope configuration cannot redirect it.
108
+ 5. Let Dependabot/Renovate open tested dependency updates in each brand-site repository.
109
+
110
+ Do not make consumer sites depend on a mutable Git branch or an unversioned remote
111
+ JavaScript URL.
112
+
113
+ For Dependabot, add this to each consumer repository's `.github/dependabot.yml`:
114
+
115
+ ```yaml
116
+ version: 2
117
+ updates:
118
+ - package-ecosystem: npm
119
+ directory: /
120
+ schedule:
121
+ interval: weekly
122
+ ```
123
+
124
+ The included `publish-brand-family.yml` workflow publishes manually after all package
125
+ and application checks pass. It expects an npm automation token in the repository secret
126
+ `NPM_TOKEN` from an account with permission to publish under the public `@168group` scope.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ import type { BrandFamilyRailProps } from "./types.js";
2
+ export declare function BrandFamilyRail({ brands, currentBrandSlug, heading, description, eyebrow, includeComingSoon, durationSeconds, openLinksInNewTab, className, style, onBrandNavigate, }: BrandFamilyRailProps): import("react/jsx-runtime").JSX.Element | null;
3
+ //# sourceMappingURL=brand-family-rail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-family-rail.d.ts","sourceRoot":"","sources":["../src/brand-family-rail.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAEV,oBAAoB,EAErB,MAAM,YAAY,CAAC;AA8IpB,wBAAgB,eAAe,CAAC,EAC9B,MAAoB,EACpB,gBAAgB,EAChB,OAA4C,EAC5C,WAAsH,EACtH,OAAgC,EAChC,iBAAyB,EACzB,eAAoB,EACpB,iBAAyB,EACzB,SAAS,EACT,KAAK,EACL,eAAe,GAChB,EAAE,oBAAoB,kDAgKtB"}
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState, } from "react";
4
+ import { brandFamily } from "./brands.js";
5
+ const cx = (...classes) => classes.filter(Boolean).join(" ");
6
+ const assetSrc = (asset) => typeof asset === "string" ? asset : asset.src;
7
+ const clampDuration = (value) => Math.min(90, Math.max(20, value));
8
+ function BrandCard({ brand, current, clone, instanceId, newTab, onBrandNavigate, }) {
9
+ const [imageFailed, setImageFailed] = useState(false);
10
+ const logoSrc = brand.logo.asset ? assetSrc(brand.logo.asset) : null;
11
+ const detailsId = `${instanceId}-${clone ? "clone" : "primary"}-${brand.slug}`;
12
+ const initials = brand.name
13
+ .split(/\s+/)
14
+ .filter(Boolean)
15
+ .slice(0, 3)
16
+ .map((word) => word[0])
17
+ .join("")
18
+ .toUpperCase();
19
+ const content = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "bfr-logo-stage", children: [imageFailed || !logoSrc ? (_jsx("span", { className: "bfr-logo-fallback", "aria-hidden": "true", children: initials })) : (_jsx("img", { className: "bfr-logo", src: logoSrc, width: brand.logo.width, height: brand.logo.height, alt: brand.logo.alt, loading: "lazy", decoding: "async", onError: () => setImageFailed(true) })), _jsx("span", { className: "bfr-card-index", "aria-hidden": "true", children: String(brand.order + 1).padStart(2, "0") })] }), _jsxs("span", { className: "bfr-card-caption", "aria-hidden": "true", children: [_jsx("span", { className: "bfr-card-caption-name", children: brand.name }), _jsx("svg", { viewBox: "0 0 16 16", width: "15", height: "15", focusable: "false", children: _jsx("path", { d: newTab ? "M5 11 11.5 4.5M7 4.5h4.5V9" : "M3.5 8h8M8.5 4l4 4-4 4", fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) })] }), _jsxs("span", { className: "bfr-card-details", id: detailsId, role: "tooltip", children: [_jsxs("span", { className: "bfr-card-heading", children: [_jsx("span", { className: "bfr-card-name", children: brand.name }), _jsx("span", { className: "bfr-card-category", children: brand.category })] }), _jsx("span", { className: "bfr-card-description", children: brand.shortDescription }), _jsxs("span", { className: "bfr-card-action", "aria-hidden": "true", children: [current ? "You are here" : "Visit website", !current && (_jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", focusable: "false", children: _jsx("path", { d: newTab ? "M5 11 11.5 4.5M7 4.5h4.5V9" : "M3.5 8h8M8.5 4l4 4-4 4", fill: "none", stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round" }) }))] })] }), current && _jsx("span", { className: "bfr-current-pill", children: "Current site" })] }));
20
+ const cardClassName = cx("bfr-card", current && "bfr-card--current", (imageFailed || !logoSrc) && "bfr-card--image-failed");
21
+ if (current || !brand.canonicalUrl) {
22
+ return (_jsx("div", { className: cardClassName, "aria-current": current ? "page" : undefined, "aria-describedby": detailsId, children: content }));
23
+ }
24
+ const handleClick = (_event) => {
25
+ try {
26
+ onBrandNavigate === null || onBrandNavigate === void 0 ? void 0 : onBrandNavigate(brand);
27
+ }
28
+ catch (error) {
29
+ console.error("[BrandFamilyRail] onBrandNavigate failed", error);
30
+ }
31
+ };
32
+ return (_jsx("a", { className: cardClassName, href: brand.canonicalUrl, target: newTab ? "_blank" : undefined, rel: newTab ? "noopener" : undefined, "aria-label": newTab ? `${brand.name} — opens in a new tab` : undefined, "aria-describedby": detailsId, "data-brand-family-slug": brand.slug, "data-brand-family-name": brand.name, tabIndex: clone ? -1 : undefined, onClick: handleClick, children: content }));
33
+ }
34
+ export function BrandFamilyRail({ brands = brandFamily, currentBrandSlug, heading = "One family. Endless discoveries.", description = "From the table to the marketplace—and everywhere people gather—meet the brands that make up 168 Group.", eyebrow = "The 168 Group family", includeComingSoon = false, durationSeconds = 48, openLinksInNewTab = false, className, style, onBrandNavigate, }) {
35
+ const instanceId = useId().replace(/:/g, "");
36
+ const viewportRef = useRef(null);
37
+ const primaryListRef = useRef(null);
38
+ const [canAnimate, setCanAnimate] = useState(false);
39
+ const [manuallyPaused, setManuallyPaused] = useState(false);
40
+ const visibleBrands = useMemo(() => [...brands]
41
+ .filter((brand) => brand.status === "active" ||
42
+ (includeComingSoon && brand.status === "coming-soon"))
43
+ .sort((a, b) => a.order - b.order), [brands, includeComingSoon]);
44
+ useEffect(() => {
45
+ const viewport = viewportRef.current;
46
+ const primaryList = primaryListRef.current;
47
+ if (!viewport || !primaryList || visibleBrands.length < 2) {
48
+ setCanAnimate(false);
49
+ return;
50
+ }
51
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
52
+ const precisePointer = window.matchMedia("(min-width: 721px) and (hover: hover) and (pointer: fine)");
53
+ const update = () => {
54
+ const overflows = primaryList.scrollWidth > viewport.clientWidth + 8;
55
+ setCanAnimate(overflows && precisePointer.matches && !reducedMotion.matches);
56
+ };
57
+ const resizeObserver = new ResizeObserver(update);
58
+ resizeObserver.observe(viewport);
59
+ resizeObserver.observe(primaryList);
60
+ reducedMotion.addEventListener("change", update);
61
+ precisePointer.addEventListener("change", update);
62
+ update();
63
+ return () => {
64
+ resizeObserver.disconnect();
65
+ reducedMotion.removeEventListener("change", update);
66
+ precisePointer.removeEventListener("change", update);
67
+ };
68
+ }, [visibleBrands.length]);
69
+ if (visibleBrands.length === 0)
70
+ return null;
71
+ const railStyle = {
72
+ ...style,
73
+ "--bfr-duration": `${clampDuration(durationSeconds)}s`,
74
+ };
75
+ return (_jsxs("section", { className: cx("bfr", canAnimate && "bfr--animated", manuallyPaused && "bfr--manual-paused", className), style: railStyle, "aria-labelledby": `${instanceId}-heading`, children: [_jsx("div", { className: "bfr-atmosphere", "aria-hidden": "true" }), _jsxs("div", { className: "bfr-header", children: [_jsx("span", { className: "bfr-seal", "aria-hidden": "true", children: _jsx("span", { children: "168" }) }), _jsxs("div", { className: "bfr-heading-copy", children: [_jsx("p", { className: "bfr-eyebrow", children: eyebrow }), _jsx("h2", { className: "bfr-title", id: `${instanceId}-heading`, children: heading }), _jsx("p", { className: "bfr-description", children: description })] }), _jsx("div", { className: "bfr-motion-slot", children: canAnimate && (_jsxs("button", { className: "bfr-motion-toggle", type: "button", "aria-pressed": manuallyPaused, onClick: () => setManuallyPaused((paused) => !paused), children: [_jsx("span", { className: "bfr-motion-icon", "aria-hidden": "true", children: manuallyPaused ? (_jsx("svg", { viewBox: "0 0 18 18", width: "16", height: "16", children: _jsx("path", { d: "m6.2 4.7 7 4.3-7 4.3V4.7Z", fill: "currentColor" }) })) : (_jsx("svg", { viewBox: "0 0 18 18", width: "16", height: "16", children: _jsx("path", { d: "M5.5 4.5h2.3v9H5.5v-9Zm4.7 0h2.3v9h-2.3v-9Z", fill: "currentColor" }) })) }), manuallyPaused ? "Resume motion" : "Pause motion"] })) })] }), _jsx("div", { className: "bfr-rail-shell", children: _jsx("div", { className: "bfr-viewport", ref: viewportRef, tabIndex: canAnimate ? undefined : 0, "aria-label": "168 Group brands", children: _jsxs("div", { className: "bfr-track", children: [_jsx("ul", { className: "bfr-list", ref: primaryListRef, children: visibleBrands.map((brand) => (_jsx("li", { className: "bfr-list-item", children: _jsx(BrandCard, { brand: brand, current: brand.slug === currentBrandSlug, clone: false, instanceId: instanceId, newTab: openLinksInNewTab, onBrandNavigate: onBrandNavigate }) }, brand.slug))) }), canAnimate && (_jsx("ul", { className: "bfr-list bfr-list--clone", "aria-hidden": "true", children: visibleBrands.map((brand) => (_jsx("li", { className: "bfr-list-item", children: _jsx(BrandCard, { brand: brand, current: brand.slug === currentBrandSlug, clone: true, instanceId: instanceId, newTab: openLinksInNewTab, onBrandNavigate: onBrandNavigate }) }, `clone-${brand.slug}`))) }))] }) }) }), _jsxs("div", { className: "bfr-footer-note", "aria-hidden": "true", children: [_jsx("span", { children: "Food" }), _jsx("i", {}), _jsx("span", { children: "Retail" }), _jsx("i", {}), _jsx("span", { children: "Entertainment" }), _jsx("i", {}), _jsx("span", { children: "Community" })] })] }));
76
+ }
77
+ //# sourceMappingURL=brand-family-rail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-family-rail.js","sourceRoot":"","sources":["../src/brand-family-rail.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { BrandFamilyEntry } from "./types.js";
2
+ export declare const brandFamily: readonly BrandFamilyEntry[];
3
+ //# sourceMappingURL=brands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brands.d.ts","sourceRoot":"","sources":["../src/brands.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAUnD,eAAO,MAAM,WAAW,6BAyHO,CAAC"}
package/dist/brands.js ADDED
@@ -0,0 +1,142 @@
1
+ import asianMartLogo from "../assets/logos/168-asian-mart.png";
2
+ import crabLogo from "../assets/logos/168-crab-karaoke.png";
3
+ import ebisuLogo from "../assets/logos/168-ebisu.png";
4
+ import groupLogo from "../assets/logos/168-group.png";
5
+ import nailSupplyLogo from "../assets/logos/168-nail-supply.png";
6
+ import asianStreetFoodLogo from "../assets/logos/asian-street-food.png";
7
+ import beyondHomeLogo from "../assets/logos/beyond-home.png";
8
+ import fujiBuffetLogo from "../assets/logos/fuji-buffet.png";
9
+ import gottaGachaLogo from "../assets/logos/gotta-gacha.png";
10
+ import laopotLogo from "../assets/logos/laopot.png";
11
+ import foundationLogo from "../assets/logos/ricky-dong-foundation.png";
12
+ import tabeLogo from "../assets/logos/tabe.png";
13
+ import { validateBrands } from "./validate-brands.js";
14
+ const logo = (asset, alt) => ({
15
+ asset,
16
+ alt,
17
+ width: 360,
18
+ height: 180,
19
+ });
20
+ export const brandFamily = validateBrands([
21
+ {
22
+ slug: "168-group",
23
+ name: "168 Group",
24
+ category: "Group",
25
+ shortDescription: "The East in Your Reach—bringing Asian food, retail, entertainment, and community together in Michigan.",
26
+ canonicalUrl: "https://www.168groupusa.com/",
27
+ logo: logo(groupLogo, "168 Group"),
28
+ order: 0,
29
+ status: "active",
30
+ },
31
+ {
32
+ slug: "168-asian-mart",
33
+ name: "168 Asian Mart",
34
+ category: "Retail",
35
+ shortDescription: "Michigan's destination for Asian groceries, fresh seafood, produce, pantry staples, and food-hall favorites.",
36
+ canonicalUrl: "https://168asianmart.com/",
37
+ logo: logo(asianMartLogo, "168 Asian Mart"),
38
+ order: 1,
39
+ status: "active",
40
+ },
41
+ {
42
+ slug: "laopot",
43
+ name: "Laopot",
44
+ category: "Restaurant",
45
+ shortDescription: "A lively all-you-can-eat hot pot and Korean barbecue experience made for gathering around the table.",
46
+ canonicalUrl: "https://www.laopot.com/",
47
+ logo: logo(laopotLogo, "Laopot"),
48
+ order: 2,
49
+ status: "active",
50
+ },
51
+ {
52
+ slug: "fuji-buffet",
53
+ name: "Fuji Buffet",
54
+ category: "Restaurant",
55
+ shortDescription: "A local Japanese buffet favorite serving fresh sushi and a generous selection of hot dishes.",
56
+ canonicalUrl: "https://www.fujibuffet.com/",
57
+ logo: logo(fujiBuffetLogo, "Fuji Buffet"),
58
+ order: 3,
59
+ status: "active",
60
+ },
61
+ {
62
+ slug: "beyond-home",
63
+ name: "Beyond Home",
64
+ category: "Retail",
65
+ shortDescription: "Unexpected Asian home goods, snacks, toys, décor, and everyday discoveries gathered under one roof.",
66
+ canonicalUrl: "https://www.beyondhomemi.com/",
67
+ logo: logo(beyondHomeLogo, "Beyond Home"),
68
+ order: 4,
69
+ status: "active",
70
+ },
71
+ {
72
+ slug: "gotta-gacha",
73
+ name: "Gotta Gacha",
74
+ category: "Entertainment",
75
+ shortDescription: "Arcade games, esports, tabletop play, karaoke, events, and collectibles in one social playground.",
76
+ canonicalUrl: "https://www.gottagacha.com/",
77
+ logo: logo(gottaGachaLogo, "Gotta Gacha"),
78
+ order: 5,
79
+ status: "active",
80
+ },
81
+ {
82
+ slug: "168-ebisu",
83
+ name: "168 Ebisu",
84
+ category: "Retail",
85
+ shortDescription: "A playful shop for Asian snacks, beauty finds, blind boxes, collectibles, and gift-worthy discoveries.",
86
+ canonicalUrl: "https://www.168ebisu.com/",
87
+ logo: logo(ebisuLogo, "168 Ebisu"),
88
+ order: 6,
89
+ status: "active",
90
+ },
91
+ {
92
+ slug: "168-crab-karaoke",
93
+ name: "168 Crab & Karaoke",
94
+ category: "Restaurant",
95
+ shortDescription: "A seafood-boil dining experience paired with private karaoke rooms for celebrations and late-night fun.",
96
+ canonicalUrl: "https://www.168crab.com/",
97
+ logo: logo(crabLogo, "168 Crab & Karaoke"),
98
+ order: 7,
99
+ status: "active",
100
+ },
101
+ {
102
+ slug: "tabe",
103
+ name: "Tabe",
104
+ category: "Restaurant",
105
+ shortDescription: "An elevated Ann Arbor dining experience with omakase, Asian-fusion plates, cocktails, and thoughtful hospitality.",
106
+ canonicalUrl: "https://tabefusion.com/",
107
+ logo: logo(tabeLogo, "Tabe"),
108
+ order: 8,
109
+ status: "active",
110
+ },
111
+ {
112
+ slug: "asian-street-food",
113
+ name: "Asian Street Food",
114
+ category: "Restaurant",
115
+ shortDescription: "A modern celebration of Asian street-food culture, from ramen and skewers to bold shareable plates.",
116
+ canonicalUrl: "https://www.eatasf.com/",
117
+ logo: logo(asianStreetFoodLogo, "Asian Street Food"),
118
+ order: 9,
119
+ status: "active",
120
+ },
121
+ {
122
+ slug: "168-nail-supply",
123
+ name: "168 Nail Supply",
124
+ category: "Retail",
125
+ shortDescription: "A wholesale-minded destination for nail color, tools, salon essentials, and professional supplies.",
126
+ canonicalUrl: "https://www.168nailsupply.com/",
127
+ logo: logo(nailSupplyLogo, "168 Nail Supply"),
128
+ order: 10,
129
+ status: "active",
130
+ },
131
+ {
132
+ slug: "ricky-dong-foundation",
133
+ name: "Ricky Dong Foundation",
134
+ category: "Community",
135
+ shortDescription: "The charitable heart of the 168 family, supporting Michigan neighbors through food, schools, and community programs.",
136
+ canonicalUrl: "https://rickydongfoundation.org/",
137
+ logo: logo(foundationLogo, "Ricky Dong Foundation"),
138
+ order: 11,
139
+ status: "active",
140
+ },
141
+ ]);
142
+ //# sourceMappingURL=brands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brands.js","sourceRoot":"","sources":["../src/brands.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { BrandFamilyRail } from "./brand-family-rail.js";
2
+ export { brandFamily } from "./brands.js";
3
+ export { validateBrands } from "./validate-brands.js";
4
+ export type { BrandFamilyCategory, BrandFamilyEntry, BrandFamilyRailProps, BrandFamilyStatus, BrandLogoAsset, } from "./types.js";
5
+ //# 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,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,cAAc,GACf,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { BrandFamilyRail } from "./brand-family-rail.js";
2
+ export { brandFamily } from "./brands.js";
3
+ export { validateBrands } from "./validate-brands.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,577 @@
1
+ .bfr {
2
+ --bfr-ink: #15110f;
3
+ --bfr-ink-soft: #201815;
4
+ --bfr-crimson: #c41e3a;
5
+ --bfr-crimson-dark: #6e1322;
6
+ --bfr-gold: #d4af37;
7
+ --bfr-cream: #fffaf0;
8
+ --bfr-muted: #d8cec2;
9
+ --bfr-line: rgba(238, 207, 120, 0.25);
10
+ --bfr-card-width: 18rem;
11
+ --bfr-card-height: 12rem;
12
+ --bfr-gap: 1.15rem;
13
+ --bfr-duration: 48s;
14
+ position: relative;
15
+ isolation: isolate;
16
+ overflow: hidden;
17
+ color: var(--bfr-cream);
18
+ background:
19
+ radial-gradient(circle at 8% 10%, rgba(196, 30, 58, 0.2), transparent 30rem),
20
+ radial-gradient(circle at 92% 90%, rgba(212, 175, 55, 0.1), transparent 26rem),
21
+ linear-gradient(140deg, #1a1210 0%, #120f0e 52%, #1b1110 100%);
22
+ border-top: 1px solid rgba(212, 175, 55, 0.35);
23
+ padding: clamp(4.5rem, 8vw, 7.5rem) 0 clamp(3.25rem, 6vw, 5rem);
24
+ font-family: "Plus Jakarta Sans", "Avenir Next", Avenir, sans-serif;
25
+ }
26
+
27
+ .bfr *,
28
+ .bfr *::before,
29
+ .bfr *::after {
30
+ box-sizing: border-box;
31
+ }
32
+
33
+ .bfr-atmosphere {
34
+ position: absolute;
35
+ z-index: -1;
36
+ inset: 0;
37
+ opacity: 0.28;
38
+ background-image:
39
+ linear-gradient(30deg, transparent 49.5%, rgba(212, 175, 55, 0.08) 50%, transparent 50.5%),
40
+ linear-gradient(150deg, transparent 49.5%, rgba(212, 175, 55, 0.06) 50%, transparent 50.5%);
41
+ background-size: 42px 74px;
42
+ mask-image: linear-gradient(to bottom, black, transparent 82%);
43
+ }
44
+
45
+ .bfr-header {
46
+ position: relative;
47
+ display: grid;
48
+ grid-template-columns: auto minmax(0, 1fr) auto;
49
+ align-items: center;
50
+ gap: clamp(1.25rem, 3vw, 2.5rem);
51
+ width: min(82rem, calc(100% - 3rem));
52
+ margin: 0 auto clamp(2.2rem, 4vw, 3.5rem);
53
+ }
54
+
55
+ .bfr-seal {
56
+ display: grid;
57
+ place-items: center;
58
+ width: clamp(4.25rem, 7vw, 5.75rem);
59
+ aspect-ratio: 1;
60
+ border: 1px solid rgba(244, 218, 143, 0.7);
61
+ border-radius: 999px;
62
+ color: var(--bfr-cream);
63
+ background: linear-gradient(145deg, #d12442, #8f1429);
64
+ box-shadow:
65
+ 0 0 0 6px rgba(196, 30, 58, 0.11),
66
+ 0 18px 55px rgba(0, 0, 0, 0.3),
67
+ inset 0 1px rgba(255, 255, 255, 0.24);
68
+ transform: rotate(-5deg);
69
+ }
70
+
71
+ .bfr-seal::before,
72
+ .bfr-seal::after {
73
+ content: "";
74
+ position: absolute;
75
+ width: 0.28rem;
76
+ height: 0.28rem;
77
+ border-radius: 50%;
78
+ background: var(--bfr-gold);
79
+ box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.12);
80
+ }
81
+
82
+ .bfr-seal::before { transform: translateY(-2rem); }
83
+ .bfr-seal::after { transform: translateY(2rem); }
84
+
85
+ .bfr-seal span {
86
+ font-family: "Playfair Display", Georgia, serif;
87
+ font-size: clamp(1.25rem, 2.6vw, 1.8rem);
88
+ font-weight: 700;
89
+ letter-spacing: -0.08em;
90
+ transform: translateX(-0.05em);
91
+ }
92
+
93
+ .bfr-eyebrow,
94
+ .bfr-title,
95
+ .bfr-description {
96
+ margin: 0;
97
+ color: inherit;
98
+ }
99
+
100
+ .bfr-eyebrow {
101
+ display: flex;
102
+ align-items: center;
103
+ gap: 0.65rem;
104
+ color: #ebcb73;
105
+ font-size: 0.68rem;
106
+ font-weight: 700;
107
+ letter-spacing: 0.22em;
108
+ line-height: 1.2;
109
+ text-transform: uppercase;
110
+ }
111
+
112
+ .bfr-eyebrow::before {
113
+ content: "";
114
+ width: 1.7rem;
115
+ height: 1px;
116
+ background: currentColor;
117
+ }
118
+
119
+ .bfr-title {
120
+ max-width: 46rem;
121
+ margin-top: 0.45rem;
122
+ font-family: "Playfair Display", Georgia, serif;
123
+ font-size: clamp(2rem, 4.2vw, 3.75rem);
124
+ font-weight: 500;
125
+ letter-spacing: -0.035em;
126
+ line-height: 1.04;
127
+ text-wrap: balance;
128
+ }
129
+
130
+ .bfr-description {
131
+ max-width: 47rem;
132
+ margin-top: 0.8rem;
133
+ color: rgba(255, 250, 240, 0.68);
134
+ font-size: clamp(0.82rem, 1.3vw, 0.95rem);
135
+ font-weight: 500;
136
+ line-height: 1.65;
137
+ }
138
+
139
+ .bfr-motion-slot {
140
+ min-width: 9rem;
141
+ justify-self: end;
142
+ }
143
+
144
+ .bfr-motion-toggle {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ gap: 0.5rem;
149
+ min-height: 2.6rem;
150
+ padding: 0.45rem 0.9rem 0.45rem 0.55rem;
151
+ border: 1px solid rgba(255, 250, 240, 0.18);
152
+ border-radius: 999px;
153
+ color: rgba(255, 250, 240, 0.78);
154
+ background: rgba(255, 255, 255, 0.045);
155
+ font: inherit;
156
+ font-size: 0.7rem;
157
+ font-weight: 700;
158
+ letter-spacing: 0.04em;
159
+ cursor: pointer;
160
+ backdrop-filter: blur(12px);
161
+ transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
162
+ }
163
+
164
+ .bfr-motion-toggle:hover {
165
+ color: var(--bfr-cream);
166
+ border-color: rgba(212, 175, 55, 0.55);
167
+ background: rgba(212, 175, 55, 0.08);
168
+ }
169
+
170
+ .bfr-motion-toggle:focus-visible,
171
+ .bfr-card:focus-visible,
172
+ .bfr-viewport:focus-visible {
173
+ outline: 3px solid #f1ce68;
174
+ outline-offset: 4px;
175
+ }
176
+
177
+ .bfr-motion-icon {
178
+ display: grid;
179
+ place-items: center;
180
+ width: 1.65rem;
181
+ height: 1.65rem;
182
+ border-radius: 50%;
183
+ color: #15110f;
184
+ background: var(--bfr-gold);
185
+ }
186
+
187
+ .bfr-rail-shell {
188
+ position: relative;
189
+ }
190
+
191
+ .bfr-rail-shell::before,
192
+ .bfr-rail-shell::after {
193
+ content: "";
194
+ position: absolute;
195
+ z-index: 3;
196
+ top: -1rem;
197
+ bottom: -1rem;
198
+ width: clamp(1.5rem, 8vw, 8rem);
199
+ pointer-events: none;
200
+ }
201
+
202
+ .bfr-rail-shell::before {
203
+ left: 0;
204
+ background: linear-gradient(to right, #17100f 5%, transparent);
205
+ }
206
+
207
+ .bfr-rail-shell::after {
208
+ right: 0;
209
+ background: linear-gradient(to left, #17100f 5%, transparent);
210
+ }
211
+
212
+ .bfr-viewport {
213
+ width: 100%;
214
+ overflow-x: auto;
215
+ overflow-y: hidden;
216
+ padding: 0.65rem 1.5rem 1rem;
217
+ scroll-padding-inline: 1.5rem;
218
+ scroll-snap-type: x proximity;
219
+ scrollbar-width: thin;
220
+ scrollbar-color: rgba(212, 175, 55, 0.5) rgba(255, 255, 255, 0.05);
221
+ }
222
+
223
+ .bfr--animated .bfr-viewport {
224
+ overflow-x: hidden;
225
+ }
226
+
227
+ .bfr-track {
228
+ display: flex;
229
+ width: max-content;
230
+ will-change: transform;
231
+ }
232
+
233
+ .bfr--animated .bfr-track {
234
+ animation: bfr-scroll var(--bfr-duration) linear infinite;
235
+ }
236
+
237
+ .bfr--manual-paused .bfr-track,
238
+ .bfr--animated .bfr-viewport:hover .bfr-track,
239
+ .bfr--animated .bfr-viewport:focus-within .bfr-track {
240
+ animation-play-state: paused;
241
+ }
242
+
243
+ .bfr-list {
244
+ display: flex;
245
+ align-items: stretch;
246
+ gap: var(--bfr-gap);
247
+ min-width: max-content;
248
+ margin: 0;
249
+ padding: 0 var(--bfr-gap) 0 0;
250
+ list-style: none;
251
+ }
252
+
253
+ .bfr-list-item {
254
+ flex: 0 0 var(--bfr-card-width);
255
+ width: var(--bfr-card-width);
256
+ scroll-snap-align: center;
257
+ }
258
+
259
+ .bfr-card {
260
+ position: relative;
261
+ display: block;
262
+ width: 100%;
263
+ height: var(--bfr-card-height);
264
+ overflow: hidden;
265
+ border: 1px solid rgba(255, 250, 240, 0.14);
266
+ border-radius: 1.1rem;
267
+ color: var(--bfr-cream);
268
+ background: #f8f2e7;
269
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.24);
270
+ text-decoration: none;
271
+ transform: translateY(0);
272
+ transition: border-color 220ms ease, box-shadow 220ms ease, transform 220ms ease;
273
+ }
274
+
275
+ .bfr-card:hover,
276
+ .bfr-card:focus-visible,
277
+ .bfr-card:focus-within {
278
+ border-color: rgba(236, 204, 111, 0.78);
279
+ box-shadow: 0 20px 55px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(212, 175, 55, 0.12);
280
+ transform: translateY(-0.24rem);
281
+ }
282
+
283
+ .bfr-logo-stage {
284
+ position: absolute;
285
+ inset: 0;
286
+ display: grid;
287
+ place-items: center;
288
+ padding: 1.25rem 1.5rem 4.35rem;
289
+ background:
290
+ radial-gradient(circle at 20% 15%, rgba(255, 255, 255, 0.9), transparent 42%),
291
+ linear-gradient(145deg, #fffdf8, #eee5d8);
292
+ }
293
+
294
+ .bfr-logo-stage::after {
295
+ content: "";
296
+ position: absolute;
297
+ inset: 0.48rem;
298
+ border: 1px solid rgba(123, 84, 40, 0.13);
299
+ border-radius: 0.82rem;
300
+ pointer-events: none;
301
+ }
302
+
303
+ .bfr-logo {
304
+ display: block;
305
+ width: 100%;
306
+ max-width: 13.5rem;
307
+ height: 6.25rem;
308
+ object-fit: contain;
309
+ filter: drop-shadow(0 5px 12px rgba(69, 41, 19, 0.1));
310
+ transition: transform 260ms ease;
311
+ }
312
+
313
+ .bfr-card:hover .bfr-logo,
314
+ .bfr-card:focus-visible .bfr-logo {
315
+ transform: scale(0.96) translateY(-0.35rem);
316
+ }
317
+
318
+ .bfr-logo-fallback {
319
+ display: grid;
320
+ place-items: center;
321
+ min-width: 4.6rem;
322
+ height: 4.6rem;
323
+ padding: 0 0.65rem;
324
+ border: 1px solid rgba(116, 27, 41, 0.3);
325
+ border-radius: 50%;
326
+ color: #81172a;
327
+ font-family: "Playfair Display", Georgia, serif;
328
+ font-size: 1.25rem;
329
+ font-weight: 700;
330
+ letter-spacing: 0.03em;
331
+ background: rgba(196, 30, 58, 0.06);
332
+ }
333
+
334
+ .bfr-card-index {
335
+ position: absolute;
336
+ top: 0.92rem;
337
+ right: 1rem;
338
+ color: rgba(81, 52, 34, 0.46);
339
+ font-family: "Playfair Display", Georgia, serif;
340
+ font-size: 0.62rem;
341
+ font-style: italic;
342
+ letter-spacing: 0.08em;
343
+ }
344
+
345
+ .bfr-card-caption {
346
+ position: absolute;
347
+ z-index: 2;
348
+ right: 0;
349
+ bottom: 0;
350
+ left: 0;
351
+ display: flex;
352
+ min-height: 4.15rem;
353
+ align-items: center;
354
+ justify-content: space-between;
355
+ gap: 1rem;
356
+ padding: 0.7rem 1.15rem 0.78rem;
357
+ color: var(--bfr-cream);
358
+ background:
359
+ linear-gradient(to right, rgba(54, 25, 29, 0.98), rgba(31, 19, 17, 0.98));
360
+ border-top: 1px solid rgba(212, 175, 55, 0.2);
361
+ transition: opacity 180ms ease, transform 220ms ease;
362
+ }
363
+
364
+ .bfr-card-caption-name {
365
+ display: -webkit-box;
366
+ overflow: hidden;
367
+ font-family: "Playfair Display", Georgia, serif;
368
+ font-size: 1rem;
369
+ font-weight: 600;
370
+ letter-spacing: -0.01em;
371
+ line-height: 1.15;
372
+ -webkit-box-orient: vertical;
373
+ -webkit-line-clamp: 2;
374
+ }
375
+
376
+ .bfr-card-caption svg {
377
+ flex: 0 0 auto;
378
+ color: #e6c768;
379
+ transition: transform 180ms ease;
380
+ }
381
+
382
+ .bfr-card:hover .bfr-card-caption,
383
+ .bfr-card:focus-visible .bfr-card-caption,
384
+ .bfr-card:focus-within .bfr-card-caption {
385
+ opacity: 0;
386
+ transform: translateY(0.65rem);
387
+ }
388
+
389
+ .bfr-card:hover .bfr-card-caption svg,
390
+ .bfr-card:focus-visible .bfr-card-caption svg {
391
+ transform: translateX(0.2rem);
392
+ }
393
+
394
+ .bfr-card-details {
395
+ position: absolute;
396
+ z-index: 2;
397
+ right: 0;
398
+ bottom: 0;
399
+ left: 0;
400
+ display: flex;
401
+ min-height: 100%;
402
+ flex-direction: column;
403
+ justify-content: flex-end;
404
+ padding: 1.15rem 1.2rem 1rem;
405
+ color: var(--bfr-cream);
406
+ background: linear-gradient(to top, rgba(25, 16, 14, 0.98) 0%, rgba(43, 22, 22, 0.95) 54%, rgba(66, 25, 33, 0.78) 74%, transparent 100%);
407
+ transform: translateY(100%);
408
+ transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1);
409
+ }
410
+
411
+ .bfr-card:hover .bfr-card-details,
412
+ .bfr-card:focus-visible .bfr-card-details,
413
+ .bfr-card:focus-within .bfr-card-details {
414
+ transform: translateY(0);
415
+ }
416
+
417
+ .bfr-card-heading {
418
+ display: flex;
419
+ align-items: baseline;
420
+ justify-content: space-between;
421
+ gap: 0.75rem;
422
+ }
423
+
424
+ .bfr-card-name {
425
+ overflow: hidden;
426
+ font-family: "Playfair Display", Georgia, serif;
427
+ font-size: 1rem;
428
+ font-weight: 600;
429
+ line-height: 1.25;
430
+ text-overflow: ellipsis;
431
+ white-space: nowrap;
432
+ }
433
+
434
+ .bfr-card-category {
435
+ flex: 0 0 auto;
436
+ color: #e6c768;
437
+ font-size: 0.55rem;
438
+ font-weight: 800;
439
+ letter-spacing: 0.13em;
440
+ text-transform: uppercase;
441
+ }
442
+
443
+ .bfr-card-description {
444
+ display: -webkit-box;
445
+ overflow: hidden;
446
+ margin-top: 0.72rem;
447
+ color: rgba(255, 250, 240, 0.76);
448
+ font-size: 0.69rem;
449
+ font-weight: 500;
450
+ line-height: 1.48;
451
+ -webkit-box-orient: vertical;
452
+ -webkit-line-clamp: 3;
453
+ }
454
+
455
+ .bfr-card-action {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 0.35rem;
459
+ margin-top: 0.72rem;
460
+ color: #edce75;
461
+ font-size: 0.61rem;
462
+ font-weight: 800;
463
+ letter-spacing: 0.09em;
464
+ text-transform: uppercase;
465
+ }
466
+
467
+ .bfr-current-pill {
468
+ position: absolute;
469
+ z-index: 4;
470
+ top: 0.85rem;
471
+ left: 0.85rem;
472
+ padding: 0.32rem 0.58rem;
473
+ border: 1px solid rgba(126, 25, 42, 0.22);
474
+ border-radius: 999px;
475
+ color: #7e192a;
476
+ background: rgba(255, 250, 240, 0.92);
477
+ font-size: 0.55rem;
478
+ font-weight: 800;
479
+ letter-spacing: 0.1em;
480
+ text-transform: uppercase;
481
+ box-shadow: 0 6px 18px rgba(59, 25, 18, 0.1);
482
+ }
483
+
484
+ .bfr-card--current {
485
+ cursor: default;
486
+ }
487
+
488
+ .bfr-footer-note {
489
+ display: flex;
490
+ align-items: center;
491
+ justify-content: center;
492
+ flex-wrap: wrap;
493
+ gap: 0.7rem;
494
+ width: min(82rem, calc(100% - 3rem));
495
+ margin: clamp(1.5rem, 3vw, 2.5rem) auto 0;
496
+ color: rgba(255, 250, 240, 0.38);
497
+ font-size: 0.58rem;
498
+ font-weight: 700;
499
+ letter-spacing: 0.18em;
500
+ text-transform: uppercase;
501
+ }
502
+
503
+ .bfr-footer-note i {
504
+ width: 0.22rem;
505
+ height: 0.22rem;
506
+ border-radius: 50%;
507
+ background: var(--bfr-gold);
508
+ box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.08);
509
+ }
510
+
511
+ @keyframes bfr-scroll {
512
+ from { transform: translate3d(0, 0, 0); }
513
+ to { transform: translate3d(-50%, 0, 0); }
514
+ }
515
+
516
+ @media (max-width: 720px), (hover: none), (pointer: coarse) {
517
+ .bfr {
518
+ --bfr-card-width: min(78vw, 17rem);
519
+ --bfr-card-height: 12.2rem;
520
+ padding-top: 4.3rem;
521
+ }
522
+
523
+ .bfr-header {
524
+ grid-template-columns: auto minmax(0, 1fr);
525
+ width: min(100% - 2rem, 42rem);
526
+ align-items: start;
527
+ }
528
+
529
+ .bfr-title { font-size: clamp(1.9rem, 9vw, 2.8rem); }
530
+ .bfr-description { font-size: 0.79rem; }
531
+ .bfr-motion-slot { display: none; }
532
+ .bfr--animated .bfr-viewport { overflow-x: auto; }
533
+ .bfr--animated .bfr-track { animation: none; transform: none; }
534
+ .bfr-rail-shell::before,
535
+ .bfr-rail-shell::after { width: 1.2rem; }
536
+ .bfr-viewport { padding-inline: 1.25rem; }
537
+ .bfr-card-details {
538
+ min-height: 6rem;
539
+ padding: 1rem;
540
+ background: linear-gradient(to top, rgba(25, 16, 14, 0.99), rgba(43, 22, 22, 0.95) 72%, transparent);
541
+ transform: none;
542
+ }
543
+ .bfr-card-caption { display: none; }
544
+ .bfr-logo-stage { padding-bottom: 5.8rem; }
545
+ .bfr-logo { height: 4.8rem; }
546
+ .bfr-card-description { -webkit-line-clamp: 2; }
547
+ .bfr-card-action { display: none; }
548
+ }
549
+
550
+ @media (max-width: 430px) {
551
+ .bfr-header {
552
+ grid-template-columns: 1fr;
553
+ text-align: center;
554
+ }
555
+
556
+ .bfr-seal { margin: 0 auto; }
557
+ .bfr-eyebrow { justify-content: center; }
558
+ .bfr-eyebrow::after {
559
+ content: "";
560
+ width: 1.7rem;
561
+ height: 1px;
562
+ background: currentColor;
563
+ }
564
+ }
565
+
566
+ @media (prefers-reduced-motion: reduce) {
567
+ .bfr *,
568
+ .bfr *::before,
569
+ .bfr *::after {
570
+ scroll-behavior: auto !important;
571
+ animation-duration: 0.01ms !important;
572
+ animation-iteration-count: 1 !important;
573
+ transition-duration: 0.01ms !important;
574
+ }
575
+
576
+ .bfr-track { transform: none !important; }
577
+ }
@@ -0,0 +1,37 @@
1
+ import type { CSSProperties } from "react";
2
+ export type BrandFamilyCategory = "Group" | "Restaurant" | "Retail" | "Entertainment" | "Community";
3
+ export type BrandFamilyStatus = "active" | "coming-soon" | "retired";
4
+ export type BrandLogoAsset = string | {
5
+ src: string;
6
+ width?: number;
7
+ height?: number;
8
+ };
9
+ export interface BrandFamilyEntry {
10
+ slug: string;
11
+ name: string;
12
+ category: BrandFamilyCategory;
13
+ shortDescription: string;
14
+ canonicalUrl?: `https://${string}`;
15
+ logo: {
16
+ asset?: BrandLogoAsset;
17
+ alt: string;
18
+ width: number;
19
+ height: number;
20
+ };
21
+ order: number;
22
+ status: BrandFamilyStatus;
23
+ }
24
+ export interface BrandFamilyRailProps {
25
+ brands?: readonly BrandFamilyEntry[];
26
+ currentBrandSlug?: string;
27
+ heading?: string;
28
+ description?: string;
29
+ eyebrow?: string;
30
+ includeComingSoon?: boolean;
31
+ durationSeconds?: number;
32
+ openLinksInNewTab?: boolean;
33
+ className?: string;
34
+ style?: CSSProperties;
35
+ onBrandNavigate?: (brand: BrandFamilyEntry) => void;
36
+ }
37
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,MAAM,MAAM,mBAAmB,GAC3B,OAAO,GACP,YAAY,GACZ,QAAQ,GACR,eAAe,GACf,WAAW,CAAC;AAEhB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,aAAa,GAAG,SAAS,CAAC;AAErE,MAAM,MAAM,cAAc,GACtB,MAAM,GACN;IACE,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,WAAW,MAAM,EAAE,CAAC;IACnC,IAAI,EAAE;QACJ,KAAK,CAAC,EAAE,cAAc,CAAC;QACvB,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACrD"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,3 @@
1
+ import type { BrandFamilyEntry } from "./types.js";
2
+ export declare const validateBrands: (brands: readonly BrandFamilyEntry[]) => readonly BrandFamilyEntry[];
3
+ //# sourceMappingURL=validate-brands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-brands.d.ts","sourceRoot":"","sources":["../src/validate-brands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAcnD,eAAO,MAAM,cAAc,WACjB,SAAS,gBAAgB,EAAE,KAClC,SAAS,gBAAgB,EAgE3B,CAAC"}
@@ -0,0 +1,67 @@
1
+ const MAX_NAME_LENGTH = 40;
2
+ const MAX_DESCRIPTION_LENGTH = 140;
3
+ const normalizeUrl = (value) => {
4
+ const url = new URL(value);
5
+ url.hash = "";
6
+ url.search = "";
7
+ url.hostname = url.hostname.toLowerCase();
8
+ url.pathname = url.pathname.replace(/\/+$/, "") || "/";
9
+ return url.toString();
10
+ };
11
+ export const validateBrands = (brands) => {
12
+ const slugs = new Set();
13
+ const urls = new Set();
14
+ const orders = new Set();
15
+ for (const brand of brands) {
16
+ const prefix = `Brand "${brand.slug || brand.name || "unknown"}"`;
17
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(brand.slug)) {
18
+ throw new Error(`${prefix} has an invalid kebab-case slug.`);
19
+ }
20
+ if (slugs.has(brand.slug)) {
21
+ throw new Error(`${prefix} duplicates an existing slug.`);
22
+ }
23
+ slugs.add(brand.slug);
24
+ if (!brand.name.trim() || brand.name.length > MAX_NAME_LENGTH) {
25
+ throw new Error(`${prefix} must have a name of 1–${MAX_NAME_LENGTH} characters.`);
26
+ }
27
+ if (!brand.shortDescription.trim() ||
28
+ brand.shortDescription.length > MAX_DESCRIPTION_LENGTH) {
29
+ throw new Error(`${prefix} must have a description of 1–${MAX_DESCRIPTION_LENGTH} characters.`);
30
+ }
31
+ if (!Number.isInteger(brand.order) || brand.order < 0) {
32
+ throw new Error(`${prefix} must have a non-negative integer order.`);
33
+ }
34
+ if (orders.has(brand.order)) {
35
+ throw new Error(`${prefix} duplicates an existing sort order.`);
36
+ }
37
+ orders.add(brand.order);
38
+ if (!brand.logo.alt.trim()) {
39
+ throw new Error(`${prefix} requires logo alt text.`);
40
+ }
41
+ if (brand.logo.width <= 0 || brand.logo.height <= 0) {
42
+ throw new Error(`${prefix} requires positive logo dimensions.`);
43
+ }
44
+ if (brand.status === "active" && !brand.canonicalUrl) {
45
+ throw new Error(`${prefix} is active and requires a canonical URL.`);
46
+ }
47
+ if (brand.canonicalUrl) {
48
+ let normalized;
49
+ try {
50
+ const parsed = new URL(brand.canonicalUrl);
51
+ if (parsed.protocol !== "https:") {
52
+ throw new Error("not HTTPS");
53
+ }
54
+ normalized = normalizeUrl(brand.canonicalUrl);
55
+ }
56
+ catch {
57
+ throw new Error(`${prefix} requires a valid HTTPS canonical URL.`);
58
+ }
59
+ if (urls.has(normalized)) {
60
+ throw new Error(`${prefix} duplicates an existing canonical URL.`);
61
+ }
62
+ urls.add(normalized);
63
+ }
64
+ }
65
+ return brands;
66
+ };
67
+ //# sourceMappingURL=validate-brands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-brands.js","sourceRoot":"","sources":["../src/validate-brands.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@168group/brand-family",
3
+ "version": "0.1.0",
4
+ "description": "The accessible, shared 168 Group brand-family rail for React websites.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./styles.css": "./dist/styles.css"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "assets/logos/168-asian-mart.png",
21
+ "assets/logos/168-crab-karaoke.png",
22
+ "assets/logos/168-ebisu.png",
23
+ "assets/logos/168-group.png",
24
+ "assets/logos/168-nail-supply.png",
25
+ "assets/logos/asian-street-food.png",
26
+ "assets/logos/beyond-home.png",
27
+ "assets/logos/fuji-buffet.png",
28
+ "assets/logos/gotta-gacha.png",
29
+ "assets/logos/laopot.png",
30
+ "assets/logos/ricky-dong-foundation.png",
31
+ "assets/logos/tabe.png",
32
+ "LICENSE",
33
+ "README.md"
34
+ ],
35
+ "sideEffects": [
36
+ "./dist/styles.css"
37
+ ],
38
+ "scripts": {
39
+ "clean": "node ./scripts/clean.mjs",
40
+ "build": "npm run clean && tsc -p tsconfig.build.json && node ./scripts/copy-assets.mjs",
41
+ "test": "vitest run --config vitest.config.ts",
42
+ "test:watch": "vitest --config vitest.config.ts",
43
+ "check": "npm run test && npm run build",
44
+ "prepack": "npm run check"
45
+ },
46
+ "peerDependencies": {
47
+ "react": "^18.0.0 || ^19.0.0",
48
+ "react-dom": "^18.0.0 || ^19.0.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "registry": "https://registry.npmjs.org/"
53
+ },
54
+ "engines": {
55
+ "node": ">=18"
56
+ },
57
+ "keywords": [
58
+ "168-group",
59
+ "brand-family",
60
+ "marquee",
61
+ "react",
62
+ "nextjs"
63
+ ]
64
+ }