@chiselandco/nexus 2.2.7 → 2.5.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 (40) hide show
  1. package/README.md +274 -286
  2. package/dist/FilterSidebar.d.ts +20 -0
  3. package/dist/FilterSidebar.d.ts.map +1 -0
  4. package/dist/FilterSidebar.js +266 -0
  5. package/dist/FilteredPortfolio.d.ts +45 -0
  6. package/dist/FilteredPortfolio.d.ts.map +1 -0
  7. package/dist/FilteredPortfolio.js +134 -0
  8. package/dist/GalleryCarousel.d.ts +9 -2
  9. package/dist/GalleryCarousel.d.ts.map +1 -1
  10. package/dist/GalleryCarousel.js +363 -63
  11. package/dist/ProjectDetail.d.ts +3 -1
  12. package/dist/ProjectDetail.d.ts.map +1 -1
  13. package/dist/ProjectDetail.js +33 -18
  14. package/dist/ProjectMenu.d.ts +8 -3
  15. package/dist/ProjectMenu.d.ts.map +1 -1
  16. package/dist/ProjectMenu.js +12 -16
  17. package/dist/ProjectMenuClient.d.ts +4 -2
  18. package/dist/ProjectMenuClient.d.ts.map +1 -1
  19. package/dist/ProjectMenuClient.js +6 -7
  20. package/dist/ProjectPortfolio.d.ts +3 -1
  21. package/dist/ProjectPortfolio.d.ts.map +1 -1
  22. package/dist/ProjectPortfolio.js +4 -4
  23. package/dist/ProjectPortfolioClient.d.ts +3 -1
  24. package/dist/ProjectPortfolioClient.d.ts.map +1 -1
  25. package/dist/ProjectPortfolioClient.js +4 -6
  26. package/dist/SimilarProjects.d.ts +3 -1
  27. package/dist/SimilarProjects.d.ts.map +1 -1
  28. package/dist/SimilarProjects.js +11 -9
  29. package/dist/index.d.ts +4 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +2 -0
  32. package/dist/types.d.ts +2 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +3 -2
  35. package/dist/ProjectFilters.d.ts +0 -11
  36. package/dist/ProjectFilters.d.ts.map +0 -1
  37. package/dist/ProjectFilters.js +0 -49
  38. package/dist/ProjectGrid.d.ts +0 -10
  39. package/dist/ProjectGrid.d.ts.map +0 -1
  40. package/dist/ProjectGrid.js +0 -8
@@ -0,0 +1,20 @@
1
+ import type { CustomFieldSchema } from "./types";
2
+ export interface FilterSidebarProps {
3
+ /**
4
+ * Schema fields to render as filter groups.
5
+ * Only select / multi-select fields with options will be used.
6
+ * If omitted, all eligible fields from the schema are shown.
7
+ */
8
+ schema: CustomFieldSchema[];
9
+ /**
10
+ * Which field keys to expose, and in what order.
11
+ * e.g. filterKeys={["application", "systems"]}
12
+ */
13
+ filterKeys?: string[];
14
+ /** Font family string matching the rest of the suite */
15
+ font?: string;
16
+ /** Label for the trigger link. Defaults to "Advanced Filters" */
17
+ triggerLabel?: string;
18
+ }
19
+ export declare function FilterSidebar({ schema, filterKeys, font, triggerLabel, }: FilterSidebarProps): import("react/jsx-runtime").JSX.Element | null;
20
+ //# sourceMappingURL=FilterSidebar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterSidebar.d.ts","sourceRoot":"","sources":["../src/FilterSidebar.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAEhD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,UAAU,EACV,IAAI,EACJ,YAAiC,GAClC,EAAE,kBAAkB,kDAuXpB"}
@@ -0,0 +1,266 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useRouter, useSearchParams, usePathname } from "next/navigation";
4
+ import { useState, useEffect, useCallback } from "react";
5
+ export function FilterSidebar({ schema, filterKeys, font, triggerLabel = "Advanced Filters", }) {
6
+ const router = useRouter();
7
+ const pathname = usePathname();
8
+ const searchParams = useSearchParams();
9
+ const [open, setOpen] = useState(false);
10
+ // Derive filter groups from schema
11
+ const filterGroups = (() => {
12
+ const eligible = schema.filter((f) => (f.type === "select" || f.type === "multi-select") &&
13
+ Array.isArray(f.options) &&
14
+ f.options.length > 0);
15
+ if (filterKeys && filterKeys.length > 0) {
16
+ return filterKeys
17
+ .map((key) => eligible.find((f) => f.key === key))
18
+ .filter((f) => !!f);
19
+ }
20
+ return eligible;
21
+ })();
22
+ // Read selected slugs for a field from URL: ?filter[fieldKey]=slug1,slug2
23
+ const getSelected = useCallback((fieldKey) => {
24
+ const raw = searchParams.get(`filter[${fieldKey}]`);
25
+ return raw ? raw.split(",").filter(Boolean) : [];
26
+ }, [searchParams]);
27
+ const buildUrl = useCallback((fieldKey, next) => {
28
+ const params = new URLSearchParams(searchParams.toString());
29
+ const paramKey = `filter[${fieldKey}]`;
30
+ if (next.length === 0) {
31
+ params.delete(paramKey);
32
+ }
33
+ else {
34
+ params.set(paramKey, next.join(","));
35
+ }
36
+ const qs = params.toString();
37
+ return qs ? `${pathname}?${qs}` : pathname;
38
+ }, [pathname, searchParams]);
39
+ const toggleOption = (fieldKey, optionId) => {
40
+ const current = getSelected(fieldKey);
41
+ const next = current.includes(optionId)
42
+ ? current.filter((v) => v !== optionId)
43
+ : [...current, optionId];
44
+ router.replace(buildUrl(fieldKey, next), { scroll: false });
45
+ };
46
+ const selectAll = (fieldKey) => {
47
+ router.replace(buildUrl(fieldKey, []), { scroll: false });
48
+ };
49
+ const clearAll = () => {
50
+ const params = new URLSearchParams(searchParams.toString());
51
+ for (const group of filterGroups) {
52
+ params.delete(`filter[${group.key}]`);
53
+ }
54
+ const qs = params.toString();
55
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
56
+ };
57
+ const activeCount = filterGroups.reduce((n, g) => n + getSelected(g.key).length, 0);
58
+ // Lock body scroll and close on Escape when open
59
+ useEffect(() => {
60
+ if (!open)
61
+ return;
62
+ const onKey = (e) => { if (e.key === "Escape")
63
+ setOpen(false); };
64
+ document.addEventListener("keydown", onKey);
65
+ document.body.style.overflow = "hidden";
66
+ return () => {
67
+ document.removeEventListener("keydown", onKey);
68
+ document.body.style.overflow = "";
69
+ };
70
+ }, [open]);
71
+ if (filterGroups.length === 0)
72
+ return null;
73
+ const f = font !== null && font !== void 0 ? font : "inherit";
74
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
75
+ .nxs-adv-trigger {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: 6px;
79
+ background: none;
80
+ border: none;
81
+ padding: 0;
82
+ cursor: pointer;
83
+ font-size: 13px;
84
+ font-weight: 600;
85
+ color: #3f3f46;
86
+ font-family: ${f};
87
+ line-height: 1;
88
+ transition: color 0.15s;
89
+ }
90
+ .nxs-adv-trigger:hover { color: #18181b; }
91
+
92
+ .nxs-adv-badge {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ background: #f18a00;
97
+ color: #fff;
98
+ font-size: 10px;
99
+ font-weight: 700;
100
+ border-radius: 999px;
101
+ min-width: 18px;
102
+ height: 18px;
103
+ padding: 0 5px;
104
+ line-height: 1;
105
+ }
106
+
107
+ .nxs-adv-backdrop {
108
+ position: fixed;
109
+ inset: 0;
110
+ background: rgba(0,0,0,0.45);
111
+ z-index: 998;
112
+ animation: nxs-fade 0.2s ease;
113
+ }
114
+ @keyframes nxs-fade { from { opacity: 0; } to { opacity: 1; } }
115
+
116
+ .nxs-adv-drawer {
117
+ position: fixed;
118
+ top: 0;
119
+ right: 0;
120
+ bottom: 0;
121
+ width: min(480px, 92vw);
122
+ background: #f4f4f5;
123
+ z-index: 999;
124
+ display: flex;
125
+ flex-direction: column;
126
+ box-shadow: -4px 0 32px rgba(0,0,0,0.14);
127
+ animation: nxs-slide 0.25s cubic-bezier(0.32,0,0,1);
128
+ font-family: ${f};
129
+ }
130
+ @keyframes nxs-slide {
131
+ from { transform: translateX(100%); }
132
+ to { transform: translateX(0); }
133
+ }
134
+
135
+ .nxs-adv-header {
136
+ display: flex;
137
+ align-items: center;
138
+ justify-content: space-between;
139
+ padding: 22px 24px 20px;
140
+ background: #fff;
141
+ border-bottom: 1px solid #e4e4e7;
142
+ flex-shrink: 0;
143
+ }
144
+ .nxs-adv-header h2 {
145
+ margin: 0;
146
+ font-size: 20px;
147
+ font-weight: 700;
148
+ color: #18181b;
149
+ letter-spacing: -0.01em;
150
+ }
151
+ .nxs-adv-close {
152
+ background: none;
153
+ border: none;
154
+ cursor: pointer;
155
+ padding: 4px;
156
+ color: #71717a;
157
+ border-radius: 4px;
158
+ display: flex;
159
+ align-items: center;
160
+ transition: color 0.15s, background 0.15s;
161
+ line-height: 0;
162
+ }
163
+ .nxs-adv-close:hover { color: #18181b; background: #f4f4f5; }
164
+
165
+ .nxs-adv-body {
166
+ flex: 1;
167
+ overflow-y: auto;
168
+ padding: 0 24px 24px;
169
+ display: flex;
170
+ flex-direction: column;
171
+ }
172
+
173
+ .nxs-adv-group {
174
+ padding: 24px 0 0;
175
+ }
176
+ .nxs-adv-group + .nxs-adv-group {
177
+ border-top: 1px solid #e4e4e7;
178
+ margin-top: 24px;
179
+ }
180
+
181
+ .nxs-adv-group-label {
182
+ font-size: 15px;
183
+ font-weight: 700;
184
+ color: #18181b;
185
+ margin: 0 0 14px 0;
186
+ }
187
+
188
+ .nxs-adv-pills {
189
+ display: flex;
190
+ flex-wrap: wrap;
191
+ gap: 8px;
192
+ }
193
+
194
+ .nxs-adv-pill {
195
+ background: none;
196
+ border: 1.5px solid #d4d4d8;
197
+ border-radius: 4px;
198
+ padding: 9px 18px;
199
+ font-size: 13px;
200
+ font-weight: 500;
201
+ color: #18181b;
202
+ cursor: pointer;
203
+ font-family: ${f};
204
+ line-height: 1.2;
205
+ transition: border-color 0.12s, background 0.12s, color 0.12s;
206
+ white-space: nowrap;
207
+ }
208
+ .nxs-adv-pill:hover {
209
+ border-color: #a1a1aa;
210
+ background: #fff;
211
+ }
212
+ .nxs-adv-pill-active {
213
+ background: #18181b;
214
+ border-color: #18181b;
215
+ color: #fff;
216
+ }
217
+ .nxs-adv-pill-active:hover {
218
+ background: #27272a;
219
+ border-color: #27272a;
220
+ }
221
+
222
+ .nxs-adv-footer {
223
+ display: flex;
224
+ gap: 12px;
225
+ padding: 16px 24px;
226
+ border-top: 1px solid #e4e4e7;
227
+ background: #fff;
228
+ flex-shrink: 0;
229
+ }
230
+ .nxs-adv-footer-btn {
231
+ flex: 1;
232
+ padding: 13px;
233
+ border-radius: 4px;
234
+ font-size: 14px;
235
+ font-weight: 600;
236
+ cursor: pointer;
237
+ border: 1.5px solid #d4d4d8;
238
+ background: #fff;
239
+ color: #18181b;
240
+ font-family: ${f};
241
+ transition: background 0.12s, border-color 0.12s;
242
+ }
243
+ .nxs-adv-footer-btn:hover { background: #f4f4f5; }
244
+ ` }), _jsxs("button", { className: "nxs-adv-trigger", onClick: () => setOpen(true), "aria-expanded": open, children: [_jsxs("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }), _jsx("line", { x1: "8", y1: "12", x2: "20", y2: "12" }), _jsx("line", { x1: "12", y1: "18", x2: "20", y2: "18" })] }), triggerLabel, activeCount > 0 && (_jsx("span", { className: "nxs-adv-badge", children: activeCount }))] }), open && (_jsxs(_Fragment, { children: [_jsx("div", { className: "nxs-adv-backdrop", onClick: () => setOpen(false), "aria-hidden": "true" }), _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": "Advanced Filters", className: "nxs-adv-drawer", children: [_jsxs("div", { className: "nxs-adv-header", children: [_jsx("h2", { children: "Advanced Filters" }), _jsx("button", { className: "nxs-adv-close", onClick: () => setOpen(false), "aria-label": "Close", children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }), _jsx("div", { className: "nxs-adv-body", children: filterGroups.map((group) => {
245
+ const selected = getSelected(group.key);
246
+ const isAllSelected = selected.length === 0;
247
+ // The API expects slugified values in filter params:
248
+ // "Operable Partitions" → "operable-partitions"
249
+ const toSlug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
250
+ const options = group.options.map((opt) => {
251
+ var _a, _b, _c;
252
+ if (typeof opt === "string") {
253
+ return { id: toSlug(opt), label: opt };
254
+ }
255
+ // If opt.id looks already slugified use it, otherwise slug the label
256
+ const id = opt.id ? opt.id : toSlug((_a = opt.label) !== null && _a !== void 0 ? _a : "");
257
+ return { id, label: (_c = (_b = opt.label) !== null && _b !== void 0 ? _b : opt.id) !== null && _c !== void 0 ? _c : "" };
258
+ }).filter((o) => o.id);
259
+ if (options.length === 0)
260
+ return null;
261
+ return (_jsxs("div", { className: "nxs-adv-group", children: [_jsx("p", { className: "nxs-adv-group-label", children: group.name }), _jsxs("div", { className: "nxs-adv-pills", children: [_jsx("button", { className: `nxs-adv-pill${isAllSelected ? " nxs-adv-pill-active" : ""}`, onClick: () => selectAll(group.key), children: "All" }), options.map((opt) => {
262
+ const isActive = selected.includes(opt.id);
263
+ return (_jsx("button", { className: `nxs-adv-pill${isActive ? " nxs-adv-pill-active" : ""}`, onClick: () => toggleOption(group.key, opt.id), "aria-pressed": isActive, children: opt.label }, opt.id));
264
+ })] })] }, group.key));
265
+ }) }), _jsxs("div", { className: "nxs-adv-footer", children: [_jsx("button", { className: "nxs-adv-footer-btn", onClick: () => setOpen(false), children: "Close" }), _jsx("button", { className: "nxs-adv-footer-btn", onClick: clearAll, children: "Clear All" })] })] })] }))] }));
266
+ }
@@ -0,0 +1,45 @@
1
+ export interface FilteredPortfolioProps {
2
+ clientSlug: string;
3
+ apiBase: string;
4
+ /** Client API key — pass via environment variable, never hardcode */
5
+ apiKey: string;
6
+ /**
7
+ * Pass Next.js searchParams here. Active filters are read from
8
+ * filter[fieldKey]=slug1,slug2 query params — set by FilterSidebar via the URL.
9
+ * These are passed directly to the API as filter params, so the API does all filtering.
10
+ */
11
+ searchParams?: Record<string, string | string[] | undefined>;
12
+ /**
13
+ * Which field keys to show as filter groups in the sidebar.
14
+ * Omit to show all eligible select/multi-select fields.
15
+ * e.g. filterKeys={["application", "systems"]}
16
+ */
17
+ filterKeys?: string[];
18
+ basePath?: string;
19
+ revalidate?: number;
20
+ noCache?: boolean;
21
+ font?: string;
22
+ }
23
+ /**
24
+ * FilteredPortfolio — server component.
25
+ *
26
+ * Passes active filter params from the URL directly to the API — the API does all filtering.
27
+ * The sidebar always shows the full schema (unfiltered options) regardless of active filters.
28
+ * Filter state is persisted in the URL as filter[fieldKey]=slug1,slug2.
29
+ *
30
+ * Usage:
31
+ * import { FilteredPortfolio } from "@chiselandco/nexus"
32
+ *
33
+ * export default async function Page({ searchParams }) {
34
+ * return (
35
+ * <FilteredPortfolio
36
+ * clientSlug="your-client-slug"
37
+ * apiBase="https://nexus.chiselandco.com"
38
+ * filterKeys={["systems", "application"]}
39
+ * searchParams={await searchParams}
40
+ * />
41
+ * )
42
+ * }
43
+ */
44
+ export declare function FilteredPortfolio({ clientSlug, apiBase, apiKey, searchParams, filterKeys, basePath, revalidate, noCache, font, }: FilteredPortfolioProps): Promise<import("react/jsx-runtime").JSX.Element>;
45
+ //# sourceMappingURL=FilteredPortfolio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilteredPortfolio.d.ts","sourceRoot":"","sources":["../src/FilteredPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;IAC5D;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AA8ED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,iBAAiB,CAAC,EACtC,UAAU,EACV,OAAO,EACP,MAAM,EACN,YAAiB,EACjB,UAAU,EACV,QAAsB,EACtB,UAAe,EACf,OAAe,EACf,IAAI,GACL,EAAE,sBAAsB,oDA8FxB"}
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { FilterSidebar } from "./FilterSidebar";
3
+ import { ProjectCard } from "./ProjectCard";
4
+ /**
5
+ * Fetch projects from the API, passing any active filter params directly.
6
+ * The API handles all filtering — AND across fields, comma-separated OR within a field.
7
+ * Schema is fetched separately (unfiltered) so the sidebar always shows all options.
8
+ */
9
+ async function fetchFilteredProjects(apiBase, clientSlug, apiKey, activeFilters, fetchOpts) {
10
+ var _a, _b;
11
+ const params = new URLSearchParams({ api_key: apiKey });
12
+ for (const [key, vals] of Object.entries(activeFilters)) {
13
+ params.set(`filter[${key}]`, vals.join(","));
14
+ }
15
+ const res = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?${params.toString()}`, fetchOpts);
16
+ const json = res.ok
17
+ ? await res.json()
18
+ : { client: { name: "", description: null, custom_fields_schema: [] }, data: [], total: 0 };
19
+ return {
20
+ projects: ((_a = json.data) !== null && _a !== void 0 ? _a : []).filter((p) => p.is_published !== false),
21
+ total: (_b = json.total) !== null && _b !== void 0 ? _b : 0,
22
+ };
23
+ }
24
+ /**
25
+ * Fetch schema only (no filters) so the sidebar always shows every available option.
26
+ */
27
+ async function fetchSchema(apiBase, clientSlug, apiKey, fetchOpts) {
28
+ var _a, _b, _c;
29
+ const res = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${apiKey}`, fetchOpts);
30
+ const json = res.ok
31
+ ? await res.json()
32
+ : { client: { name: "", description: null, custom_fields_schema: [] }, data: [], total: 0 };
33
+ const seen = new Set();
34
+ const schema = ((_b = (_a = json.client) === null || _a === void 0 ? void 0 : _a.custom_fields_schema) !== null && _b !== void 0 ? _b : []).filter((f) => {
35
+ if (seen.has(f.key))
36
+ return false;
37
+ seen.add(f.key);
38
+ return true;
39
+ });
40
+ const fieldOptionsMap = {};
41
+ for (const field of schema) {
42
+ const map = {};
43
+ for (const opt of (_c = field.options) !== null && _c !== void 0 ? _c : []) {
44
+ if (typeof opt === "object" && opt.id && opt.label) {
45
+ map[opt.id] = opt.label;
46
+ map[opt.label] = opt.label;
47
+ }
48
+ else if (typeof opt === "string") {
49
+ map[opt] = opt;
50
+ }
51
+ }
52
+ fieldOptionsMap[field.key] = map;
53
+ }
54
+ return { schema, fieldOptionsMap };
55
+ }
56
+ /**
57
+ * FilteredPortfolio — server component.
58
+ *
59
+ * Passes active filter params from the URL directly to the API — the API does all filtering.
60
+ * The sidebar always shows the full schema (unfiltered options) regardless of active filters.
61
+ * Filter state is persisted in the URL as filter[fieldKey]=slug1,slug2.
62
+ *
63
+ * Usage:
64
+ * import { FilteredPortfolio } from "@chiselandco/nexus"
65
+ *
66
+ * export default async function Page({ searchParams }) {
67
+ * return (
68
+ * <FilteredPortfolio
69
+ * clientSlug="your-client-slug"
70
+ * apiBase="https://nexus.chiselandco.com"
71
+ * filterKeys={["systems", "application"]}
72
+ * searchParams={await searchParams}
73
+ * />
74
+ * )
75
+ * }
76
+ */
77
+ export async function FilteredPortfolio({ clientSlug, apiBase, apiKey, searchParams = {}, filterKeys, basePath = "/projects", revalidate = 60, noCache = false, font, }) {
78
+ const fetchOpts = noCache
79
+ ? { cache: "no-store" }
80
+ : { next: { revalidate } };
81
+ // Parse active filters from URL: filter[fieldKey]=slug1,slug2
82
+ const activeFilters = {};
83
+ for (const [key, val] of Object.entries(searchParams)) {
84
+ const match = key.match(/^filter\[(.+)\]$/);
85
+ if (!match || !val)
86
+ continue;
87
+ const fieldKey = match[1];
88
+ const slugs = (Array.isArray(val) ? val[0] : val).split(",").filter(Boolean);
89
+ if (slugs.length > 0)
90
+ activeFilters[fieldKey] = slugs;
91
+ }
92
+ // Fetch in parallel: filtered projects + full schema for the sidebar
93
+ const [{ projects, total }, { schema, fieldOptionsMap }] = await Promise.all([
94
+ fetchFilteredProjects(apiBase, clientSlug, apiKey, activeFilters, fetchOpts),
95
+ fetchSchema(apiBase, clientSlug, apiKey, fetchOpts),
96
+ ]);
97
+ const activeCount = Object.values(activeFilters).reduce((n, ids) => n + ids.length, 0);
98
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
99
+ .nxs-fp-wrap {
100
+ width: 100%;
101
+ max-width: 1280px;
102
+ margin: 0 auto;
103
+ padding: 2rem 1rem;
104
+ box-sizing: border-box;
105
+ }
106
+ .nxs-fp-toolbar {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 16px;
110
+ flex-wrap: wrap;
111
+ margin-bottom: 1.5rem;
112
+ }
113
+ .nxs-fp-count {
114
+ font-size: 13px;
115
+ color: #71717a;
116
+ flex: 1;
117
+ }
118
+ .nxs-fp-count strong { color: #18181b; }
119
+ .nxs-fp-grid {
120
+ display: grid;
121
+ grid-template-columns: 1fr;
122
+ gap: 1.5rem;
123
+ }
124
+ @media (min-width: 640px) {
125
+ .nxs-fp-grid { grid-template-columns: repeat(2, 1fr); }
126
+ }
127
+ @media (min-width: 1024px) {
128
+ .nxs-fp-grid { grid-template-columns: repeat(3, 1fr); gap: 2rem; }
129
+ }
130
+ .chisel-project-card-img { height: 180px; }
131
+ @media (min-width: 640px) { .chisel-project-card-img { height: 200px; } }
132
+ @media (min-width: 1024px) { .chisel-project-card-img { height: 220px; } }
133
+ ` }), _jsxs("div", { className: "nxs-fp-wrap", children: [_jsxs("div", { className: "nxs-fp-toolbar", children: [_jsxs("p", { className: "nxs-fp-count", children: ["Showing ", _jsx("strong", { children: projects.length }), total > projects.length ? ` of ${total}` : "", " project", projects.length !== 1 ? "s" : "", activeCount > 0 && ` (${activeCount} filter${activeCount !== 1 ? "s" : ""} active)`] }), _jsx(FilterSidebar, { schema: schema, filterKeys: filterKeys, font: font })] }), projects.length === 0 ? (_jsx("div", { style: { padding: "4rem 0", textAlign: "center" }, children: _jsx("p", { style: { color: "#71717a" }, children: "No projects match the selected filters." }) })) : (_jsx("div", { className: "nxs-fp-grid", children: projects.map((project, index) => (_jsx(ProjectCard, { project: project, schema: schema, fieldOptionsMap: fieldOptionsMap, basePath: basePath, priority: index === 0 }, project.id))) }))] })] }));
134
+ }
@@ -1,7 +1,14 @@
1
- import type { Media } from "./types";
1
+ import type { Media, CustomFieldSchema } from "./types";
2
2
  export interface GalleryCarouselProps {
3
3
  images: Media[];
4
4
  projectTitle: string;
5
+ /**
6
+ * Client custom fields schema — used to resolve slug values to human-readable
7
+ * labels for tag pills and filter chips. All fields that have values on at
8
+ * least one image are surfaced as filterable dimensions automatically.
9
+ * If omitted, raw slug values are humanised as fallback.
10
+ */
11
+ schema?: CustomFieldSchema[];
5
12
  }
6
- export declare function GalleryCarousel({ images, projectTitle, }: GalleryCarouselProps): import("react/jsx-runtime").JSX.Element | null;
13
+ export declare function GalleryCarousel({ images, projectTitle, schema }: GalleryCarouselProps): import("react/jsx-runtime").JSX.Element | null;
7
14
  //# sourceMappingURL=GalleryCarousel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,YAAY,GACb,EAAE,oBAAoB,kDAwJtB"}
1
+ {"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,iBAAiB,EAAe,MAAM,SAAS,CAAA;AASpE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;CAC7B;AA+ND,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAW,EAAE,EAAE,oBAAoB,kDA2V1F"}