project-portfolio 2.0.0 → 2.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.
@@ -17,7 +17,7 @@ const ACCENT = "oklch(0.78 0.16 85)";
17
17
  const API_KEY = "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR";
18
18
  const DEFAULT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
19
19
  const menuDataCache = new Map();
20
- export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: projectsProp, schema: schemaProp, filterOptions: filterOptionsProp, filterFieldKey: filterFieldKeyProp, filterFieldName: filterFieldNameProp = "Project Type", fieldOptionsMap: fieldOptionsMapProp = {}, subtitle, basePath, viewAllPath, font = DEFAULT_FONT, maxProjects = 6, }) {
20
+ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projects: projectsProp, schema: schemaProp, filterOptions: filterOptionsProp, filterFieldKey: filterFieldKeyProp, filterFieldName: filterFieldNameProp = "Project Type", fieldOptionsMap: fieldOptionsMapProp = {}, subtitle, basePath, viewAllPath, font = DEFAULT_FONT, maxProjects = 6, }) {
21
21
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
22
22
  const [filtersOpen, setFiltersOpen] = useState(false);
23
23
  const [hoveredCard, setHoveredCard] = useState(null);
@@ -30,9 +30,9 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
30
30
  if (!hasDataUrl && !hasDirectFetch)
31
31
  return;
32
32
  let cancelled = false;
33
- const cacheKey = dataUrl !== null && dataUrl !== void 0 ? dataUrl : `${clientSlug}:${apiBase}`;
33
+ const cacheKey = dataUrl !== null && dataUrl !== void 0 ? dataUrl : `${clientSlug}:${apiBase}:${menuId !== null && menuId !== void 0 ? menuId : "all"}`;
34
34
  async function fetchAndCache() {
35
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
35
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
36
36
  // dataUrl mode: fetch from local API route (server-cached, no API key exposed)
37
37
  if (dataUrl) {
38
38
  const res = await fetch(dataUrl);
@@ -46,19 +46,62 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
46
46
  fieldOptionsMap: (_f = json.fieldOptionsMap) !== null && _f !== void 0 ? _f : {},
47
47
  };
48
48
  }
49
- // Direct fetch mode: fetch from the upstream API directly
49
+ // Menu endpoint mode: fetch from /menus/{menuId} when menuId is provided
50
+ // Also fetch /projects to get schema for filters (menu endpoint may not return it)
51
+ if (menuId) {
52
+ const [menuRes, projectsRes, fieldsRes] = await Promise.all([
53
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`),
54
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`),
55
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`),
56
+ ]);
57
+ const menuJson = menuRes.ok ? await menuRes.json() : {};
58
+ const projectsJson = projectsRes.ok ? await projectsRes.json() : {};
59
+ const projects = (_h = (_g = menuJson === null || menuJson === void 0 ? void 0 : menuJson.projects) !== null && _g !== void 0 ? _g : menuJson === null || menuJson === void 0 ? void 0 : menuJson.data) !== null && _h !== void 0 ? _h : [];
60
+ // Get schema from menu response first, fallback to projects response
61
+ const schema = (_o = (_l = (_k = (_j = menuJson === null || menuJson === void 0 ? void 0 : menuJson.client) === null || _j === void 0 ? void 0 : _j.custom_fields_schema) !== null && _k !== void 0 ? _k : menuJson === null || menuJson === void 0 ? void 0 : menuJson.schema) !== null && _l !== void 0 ? _l : (_m = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.client) === null || _m === void 0 ? void 0 : _m.custom_fields_schema) !== null && _o !== void 0 ? _o : [];
62
+ const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
63
+ const fieldOptionsMap = {};
64
+ for (const field of ((_p = fieldsJson.fields) !== null && _p !== void 0 ? _p : [])) {
65
+ const map = {};
66
+ for (const opt of ((_q = field.options) !== null && _q !== void 0 ? _q : [])) {
67
+ if (typeof opt === "object" && opt.id && opt.label) {
68
+ map[opt.id] = opt.label;
69
+ map[opt.label] = opt.label;
70
+ }
71
+ }
72
+ fieldOptionsMap[field.key] = map;
73
+ }
74
+ const filterField = (_r = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _r !== void 0 ? _r : null;
75
+ const filterOptions = filterField
76
+ ? ((_s = filterField.options) !== null && _s !== void 0 ? _s : []).map((opt) => {
77
+ var _a, _b;
78
+ if (typeof opt === "string")
79
+ return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
80
+ return { id: (_a = opt.id) !== null && _a !== void 0 ? _a : "", label: (_b = opt.label) !== null && _b !== void 0 ? _b : "" };
81
+ })
82
+ : [];
83
+ return {
84
+ projects,
85
+ schema,
86
+ filterOptions,
87
+ filterFieldKey: (_t = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _t !== void 0 ? _t : null,
88
+ filterFieldName: (_u = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _u !== void 0 ? _u : "Project Type",
89
+ fieldOptionsMap,
90
+ };
91
+ }
92
+ // Direct fetch mode: fetch from the upstream API directly (all projects)
50
93
  const [projectsRes, fieldsRes] = await Promise.all([
51
94
  fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`),
52
95
  fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`),
53
96
  ]);
54
97
  const json = projectsRes.ok ? await projectsRes.json() : {};
55
- const projects = (_g = json === null || json === void 0 ? void 0 : json.data) !== null && _g !== void 0 ? _g : [];
56
- const schema = (_j = (_h = json === null || json === void 0 ? void 0 : json.client) === null || _h === void 0 ? void 0 : _h.custom_fields_schema) !== null && _j !== void 0 ? _j : [];
98
+ const projects = (_v = json === null || json === void 0 ? void 0 : json.data) !== null && _v !== void 0 ? _v : [];
99
+ const schema = (_x = (_w = json === null || json === void 0 ? void 0 : json.client) === null || _w === void 0 ? void 0 : _w.custom_fields_schema) !== null && _x !== void 0 ? _x : [];
57
100
  const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
58
101
  const fieldOptionsMap = {};
59
- for (const field of ((_k = fieldsJson.fields) !== null && _k !== void 0 ? _k : [])) {
102
+ for (const field of ((_y = fieldsJson.fields) !== null && _y !== void 0 ? _y : [])) {
60
103
  const map = {};
61
- for (const opt of ((_l = field.options) !== null && _l !== void 0 ? _l : [])) {
104
+ for (const opt of ((_z = field.options) !== null && _z !== void 0 ? _z : [])) {
62
105
  if (typeof opt === "object" && opt.id && opt.label) {
63
106
  map[opt.id] = opt.label;
64
107
  map[opt.label] = opt.label;
@@ -66,9 +109,9 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
66
109
  }
67
110
  fieldOptionsMap[field.key] = map;
68
111
  }
69
- const filterField = (_m = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _m !== void 0 ? _m : null;
112
+ const filterField = (_0 = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _0 !== void 0 ? _0 : null;
70
113
  const filterOptions = filterField
71
- ? ((_o = filterField.options) !== null && _o !== void 0 ? _o : []).map((opt) => {
114
+ ? ((_1 = filterField.options) !== null && _1 !== void 0 ? _1 : []).map((opt) => {
72
115
  var _a, _b;
73
116
  if (typeof opt === "string")
74
117
  return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
@@ -79,8 +122,8 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
79
122
  projects,
80
123
  schema,
81
124
  filterOptions,
82
- filterFieldKey: (_p = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _p !== void 0 ? _p : null,
83
- filterFieldName: (_q = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _q !== void 0 ? _q : "Project Type",
125
+ filterFieldKey: (_2 = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _2 !== void 0 ? _2 : null,
126
+ filterFieldName: (_3 = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _3 !== void 0 ? _3 : "Project Type",
84
127
  fieldOptionsMap,
85
128
  };
86
129
  }
@@ -95,7 +138,7 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
95
138
  // silently fail — render nothing
96
139
  });
97
140
  return () => { cancelled = true; };
98
- }, [dataUrl, clientSlug, apiBase]);
141
+ }, [dataUrl, clientSlug, apiBase, menuId]);
99
142
  // Resolve data: prefer self-fetched, fall back to props
100
143
  const projects = (_b = (_a = fetched === null || fetched === void 0 ? void 0 : fetched.projects) !== null && _a !== void 0 ? _a : projectsProp) !== null && _b !== void 0 ? _b : [];
101
144
  const schema = (_d = (_c = fetched === null || fetched === void 0 ? void 0 : fetched.schema) !== null && _c !== void 0 ? _c : schemaProp) !== null && _d !== void 0 ? _d : [];
@@ -118,7 +161,7 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
118
161
  font-family: ${font};
119
162
  width: 100%;
120
163
  box-sizing: border-box;
121
- padding: 1.5rem 1rem;
164
+ padding: 1.25rem 1rem;
122
165
  }
123
166
  @media (min-width: 768px) {
124
167
  .chisel-menu-outer {
@@ -127,19 +170,23 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
127
170
  }
128
171
  }
129
172
 
173
+ /* ── Left column ── */
130
174
  .chisel-menu-left {
131
175
  flex: 1;
132
176
  min-width: 0;
133
177
  padding-right: 0;
134
- padding-bottom: 1.5rem;
178
+ padding-bottom: 1.25rem;
179
+ border-bottom: 1px solid #e4e4e7;
135
180
  }
136
181
  @media (min-width: 768px) {
137
182
  .chisel-menu-left {
138
183
  padding-right: 2.5rem;
139
184
  padding-bottom: 0;
185
+ border-bottom: none;
140
186
  }
141
187
  }
142
188
 
189
+ /* ── Vertical divider — desktop only ── */
143
190
  .chisel-menu-divider-v {
144
191
  display: none;
145
192
  }
@@ -153,79 +200,128 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
153
200
  }
154
201
  }
155
202
 
203
+ /* ── Right column ── */
156
204
  .chisel-menu-right {
157
205
  width: 100%;
158
206
  padding-left: 0;
159
- padding-top: 1.5rem;
160
- border-top: 1px solid #e4e4e7;
207
+ padding-top: 1.25rem;
161
208
  }
162
209
  @media (min-width: 768px) {
163
210
  .chisel-menu-right {
164
- width: 360px;
211
+ width: 220px;
165
212
  flex-shrink: 0;
166
213
  padding-left: 2.5rem;
167
214
  padding-top: 0;
168
- border-top: none;
215
+ }
216
+ }
217
+ @media (min-width: 1024px) {
218
+ .chisel-menu-right {
219
+ width: 280px;
169
220
  }
170
221
  }
171
222
 
223
+ /* ── Project card grid ──
224
+ Mobile: 1 column — cards get full width, titles don't wrap
225
+ Desktop: 2 columns */
172
226
  .chisel-menu-card-grid {
173
227
  display: grid;
174
228
  grid-template-columns: 1fr;
175
- gap: 10px;
229
+ gap: 6px;
176
230
  }
177
- @media (min-width: 540px) {
231
+ @media (min-width: 768px) {
178
232
  .chisel-menu-card-grid {
179
233
  grid-template-columns: repeat(2, 1fr);
180
- gap: 12px;
234
+ gap: 10px;
181
235
  }
182
236
  }
183
237
 
184
- .chisel-menu-filters-mobile-toggle {
238
+ /* Hide cards beyond the 3rd on mobile */
239
+ @media (max-width: 767px) {
240
+ .chisel-menu-card:nth-child(n+4) {
241
+ display: none;
242
+ }
243
+ }
244
+
245
+ /* Card — always horizontal side-by-side */
246
+ .chisel-menu-card {
185
247
  display: flex;
248
+ flex-direction: row;
186
249
  align-items: center;
187
- justify-content: space-between;
188
- background: none;
189
- border: none;
190
- padding: 0;
250
+ gap: 12px;
251
+ padding: 10px 12px;
252
+ border: 1px solid #e4e4e7;
253
+ text-decoration: none;
254
+ color: inherit;
255
+ box-sizing: border-box;
256
+ transition: border-color 0.2s;
191
257
  cursor: pointer;
192
- width: 100%;
193
- margin-bottom: 10px;
258
+ overflow: hidden;
259
+ }
260
+ .chisel-menu-card:hover {
261
+ border-color: ${ACCENT};
262
+ }
263
+
264
+ .chisel-menu-card-thumb {
265
+ width: 72px;
266
+ height: 54px;
267
+ overflow: hidden;
268
+ background-color: #f4f4f5;
269
+ flex-shrink: 0;
194
270
  }
195
271
  @media (min-width: 768px) {
196
- .chisel-menu-filters-mobile-toggle {
197
- display: none;
272
+ .chisel-menu-card-thumb {
273
+ width: 72px;
274
+ height: 54px;
198
275
  }
199
276
  }
200
277
 
201
- .chisel-menu-filters-list {
202
- display: none;
203
- }
204
- .chisel-menu-filters-list.open {
278
+ .chisel-menu-card-body {
205
279
  display: flex;
206
280
  flex-direction: column;
207
281
  gap: 2px;
282
+ flex: 1;
283
+ min-width: 0;
284
+ }
285
+
286
+ /* ── Subtitle — hidden on mobile, shown on desktop ── */
287
+ .chisel-menu-subtitle {
288
+ display: none !important;
208
289
  }
209
290
  @media (min-width: 768px) {
210
- .chisel-menu-filters-list {
211
- display: flex !important;
212
- flex-direction: column;
213
- gap: 2px;
291
+ .chisel-menu-subtitle {
292
+ display: block !important;
214
293
  }
215
294
  }
295
+
296
+ /* ── Filter links — always visible, no toggle needed ── */
297
+ .chisel-menu-filter-link {
298
+ all: unset;
299
+ cursor: pointer;
300
+ display: block;
301
+ font-size: 14px;
302
+ color: #52525b;
303
+ font-weight: 400;
304
+ padding: 7px 0;
305
+ text-decoration: none;
306
+ transition: color 0.15s;
307
+ }
308
+ .chisel-menu-filter-link:hover {
309
+ color: #18181b;
310
+ }
216
311
  ` }), _jsxs("div", { className: "chisel-menu-outer", children: [_jsxs("div", { className: "chisel-menu-left", children: [_jsx("p", { style: {
217
- fontSize: "13px",
312
+ fontSize: "11px",
218
313
  fontWeight: 700,
219
314
  textTransform: "uppercase",
220
315
  letterSpacing: "0.12em",
221
316
  color: "#71717a",
222
- margin: "0 0 14px 0",
317
+ margin: "0 0 12px 0",
223
318
  }, children: "Featured Projects" }), subtitle && (_jsx("p", { style: {
224
- fontSize: "16px",
319
+ fontSize: "14px",
225
320
  color: "#52525b",
226
- lineHeight: 1.6,
227
- margin: "0 0 24px 0",
228
- }, children: subtitle })), displayed.length === 0 ? (_jsx("p", { style: { fontSize: "14px", color: "#a1a1aa", margin: 0 }, children: "No projects found." })) : (_jsx("div", { className: "chisel-menu-card-grid", children: displayed.map((project) => {
321
+ lineHeight: 1.5,
322
+ margin: "0 0 16px 0",
323
+ display: "none",
324
+ }, className: "chisel-menu-subtitle", children: subtitle })), displayed.length === 0 ? (_jsx("p", { style: { fontSize: "14px", color: "#a1a1aa", margin: 0 }, children: "No projects found." })) : (_jsx("div", { className: "chisel-menu-card-grid", children: displayed.map((project) => {
229
325
  var _a, _b, _c, _d, _e, _f;
230
326
  const imageUrl = (_d = (_a = project.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = project.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : null;
231
327
  const badgeRaw = badgeField ? parseSingleValue(project.custom_field_values[badgeField.key]) : null;
@@ -234,78 +330,46 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
234
330
  const tags = tagsField ? parseMultiValue(project.custom_field_values[tagsField.key]) : [];
235
331
  const href = `${basePath}/${project.slug}`;
236
332
  const isHovered = hoveredCard === project.id;
237
- return (_jsxs("a", { href: href, onMouseEnter: () => setHoveredCard(project.id), onMouseLeave: () => setHoveredCard(null), style: {
238
- all: "unset",
239
- cursor: "pointer",
240
- display: "flex",
241
- alignItems: "flex-start",
242
- gap: "12px",
243
- padding: "12px",
244
- border: `1px solid ${isHovered ? ACCENT : "#e4e4e7"}`,
245
- textDecoration: "none",
246
- boxSizing: "border-box",
247
- transition: "border-color 0.2s",
248
- }, children: [_jsx("div", { style: {
249
- width: "80px",
250
- height: "64px",
251
- flexShrink: 0,
252
- overflow: "hidden",
253
- backgroundColor: "#f4f4f5",
254
- }, children: imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: {
333
+ return (_jsxs("a", { href: href, className: "chisel-menu-card", onMouseEnter: () => setHoveredCard(project.id), onMouseLeave: () => setHoveredCard(null), children: [_jsx("div", { className: "chisel-menu-card-thumb", children: imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: {
255
334
  width: "100%",
256
335
  height: "100%",
257
336
  objectFit: "cover",
258
337
  display: "block",
259
- transform: isHovered ? "scale(1.06)" : "scale(1)",
338
+ transform: isHovered ? "scale(1.05)" : "scale(1)",
260
339
  transition: "transform 0.3s ease",
261
- } })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "3px", minWidth: 0, flex: 1 }, children: [_jsx("p", { style: {
262
- fontSize: "14px",
340
+ } })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", { className: "chisel-menu-card-body", children: [_jsx("p", { style: {
341
+ fontSize: "13px",
263
342
  fontWeight: 700,
264
343
  color: isHovered ? ACCENT : "#18181b",
265
344
  margin: 0,
266
345
  lineHeight: 1.3,
267
346
  fontFamily: font,
268
347
  transition: "color 0.2s",
269
- }, children: project.title }), badge && (_jsx("p", { style: { fontSize: "13px", color: "#52525b", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (() => {
348
+ }, children: project.title }), badge && (_jsx("p", { style: { fontSize: "12px", color: "#71717a", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (() => {
270
349
  var _a;
271
350
  const optMap = tagsField ? ((_a = fieldOptionsMap[tagsField.key]) !== null && _a !== void 0 ? _a : {}) : {};
272
- const tagLabels = tags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; });
273
- const shown = tagLabels.slice(0, 3);
274
- const display = tagLabels.length > 3
275
- ? shown.join(" · ") + " · ..."
276
- : shown.join(" · ");
277
- return (_jsx("p", { style: { fontSize: "12px", color: ACCENT, margin: 0, fontFamily: font, lineHeight: 1.4 }, children: display }));
351
+ const tagLabels = tags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; }).slice(0, 2);
352
+ return (_jsx("p", { style: { fontSize: "11px", color: ACCENT, margin: 0, fontFamily: font, lineHeight: 1.4 }, children: tagLabels.join(" · ") }));
278
353
  })()] })] }, project.id));
279
- }) }))] }), _jsx("div", { className: "chisel-menu-divider-v" }), filterOptions.length > 0 && (_jsxs("div", { className: "chisel-menu-right", children: [_jsx("p", { style: {
280
- fontSize: "11px",
281
- fontWeight: 700,
282
- textTransform: "uppercase",
283
- letterSpacing: "0.12em",
284
- color: "#71717a",
285
- margin: "0 0 14px 0",
286
- }, children: "Browse By" }), _jsx("p", { style: {
287
- fontSize: "15px",
288
- fontWeight: 700,
289
- color: "#18181b",
290
- margin: "0 0 10px 0",
291
- fontFamily: font,
292
- }, children: filterFieldName }), _jsx("button", { className: "chisel-menu-filters-mobile-toggle", onClick: () => setFiltersOpen((o) => !o), style: { marginTop: "-10px" }, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#18181b", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", style: { transform: filtersOpen ? "rotate(180deg)" : "rotate(0deg)", transition: "transform 0.2s", flexShrink: 0 }, children: _jsx("path", { d: "M6 9l6 6 6-6" }) }) }), _jsx("div", { className: `chisel-menu-filters-list${filtersOpen ? " open" : ""}`, children: filterOptions.map((opt) => {
293
- const href = filterFieldKey
294
- ? `${basePath}?filter[${filterFieldKey}]=${encodeURIComponent(opt.id)}`
295
- : basePath;
296
- return (_jsx("a", { href: href, style: {
297
- all: "unset",
298
- cursor: "pointer",
299
- fontSize: "14px",
300
- color: "#52525b",
301
- fontWeight: 400,
302
- padding: "5px 0",
303
- textDecoration: "none",
354
+ }) }))] }), _jsx("div", { className: "chisel-menu-divider-v" }), _jsxs("div", { className: "chisel-menu-right", children: [filterOptions.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { style: {
355
+ fontSize: "11px",
356
+ fontWeight: 700,
357
+ textTransform: "uppercase",
358
+ letterSpacing: "0.12em",
359
+ color: "#71717a",
360
+ margin: "0 0 10px 0",
361
+ }, children: "Browse By" }), _jsx("p", { style: {
362
+ fontSize: "15px",
363
+ fontWeight: 700,
364
+ color: "#18181b",
365
+ margin: "0 0 4px 0",
304
366
  fontFamily: font,
305
- transition: "color 0.15s",
306
- display: "block",
307
- }, onMouseEnter: (e) => (e.currentTarget.style.color = "#18181b"), onMouseLeave: (e) => (e.currentTarget.style.color = "#52525b"), children: opt.label }, opt.id));
308
- }) }), _jsx("div", { style: { height: "1px", backgroundColor: "#e4e4e7", margin: "20px 0" } }), _jsxs("a", { href: viewAllPath, style: {
367
+ }, children: filterFieldName }), _jsx("div", { style: { display: "flex", flexDirection: "column", marginBottom: "20px" }, children: filterOptions.map((opt) => {
368
+ const href = filterFieldKey
369
+ ? `${basePath}?filter[${filterFieldKey}]=${encodeURIComponent(opt.id)}`
370
+ : basePath;
371
+ return (_jsx("a", { href: href, className: "chisel-menu-filter-link", style: { fontFamily: font }, children: opt.label }, opt.id));
372
+ }) }), _jsx("div", { style: { height: "1px", backgroundColor: "#e4e4e7", marginBottom: "20px" } })] })), _jsxs("a", { href: viewAllPath, style: {
309
373
  all: "unset",
310
374
  cursor: "pointer",
311
375
  display: "inline-flex",
@@ -316,5 +380,5 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
316
380
  color: ACCENT,
317
381
  textDecoration: "none",
318
382
  fontFamily: font,
319
- }, children: ["View All Projects", _jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] })] }))] })] }));
383
+ }, children: ["View All Projects", _jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] })] })] })] }));
320
384
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectPortfolioClient.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolioClient.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,2BAA2B;IAC1C,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;CAChB;AA0DD,wBAAgB,sBAAsB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,OAAY,EACZ,IAAmB,EACnB,OAAW,GACZ,EAAE,2BAA2B,2CAsJ7B"}
1
+ {"version":3,"file":"ProjectPortfolioClient.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolioClient.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,2BAA2B;IAC1C,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;CAChB;AA0DD,wBAAgB,sBAAsB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,OAAY,EACZ,IAAmB,EACnB,OAAW,GACZ,EAAE,2BAA2B,2CAoJ7B"}
@@ -108,7 +108,12 @@ export function ProjectPortfolioClient({ clientSlug, apiBase, basePath = "/proje
108
108
  margin: "0 auto",
109
109
  padding: "2rem 1rem",
110
110
  fontFamily: font,
111
- }, children: [_jsx("div", { style: { display: "flex", gap: "1rem", marginBottom: "2rem", paddingBottom: "2rem", borderBottom: "1px solid #e4e4e7" }, children: [1, 2].map((i) => (_jsx("div", { style: { width: 180, height: 60, backgroundColor: "#f4f4f5", borderRadius: 6 } }, i))) }), _jsx("div", { style: { display: "grid", gridTemplateColumns: gridCols, gap: "2rem" }, children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { style: { backgroundColor: "#f4f4f5", borderRadius: 2, height: 320, animation: "pulse 1.5s ease-in-out infinite" } }, i))) }), _jsx("style", { children: `@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.5 } }` })] }));
111
+ }, children: [_jsx("style", { children: `
112
+ @keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.5 } }
113
+ .chisel-skeleton-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
114
+ @media (min-width: 640px) { .chisel-skeleton-grid { grid-template-columns: repeat(2, 1fr); } }
115
+ @media (min-width: 1024px) { .chisel-skeleton-grid { grid-template-columns: ${gridCols}; gap: 2rem; } }
116
+ ` }), _jsx("div", { className: "chisel-skeleton-grid", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { style: { backgroundColor: "#f4f4f5", borderRadius: 2, height: 320, animation: "pulse 1.5s ease-in-out infinite" } }, i))) })] }));
112
117
  }
113
118
  return (_jsxs("div", { style: {
114
119
  width: "100%",
@@ -20,6 +20,18 @@ export interface SimilarProjectsProps {
20
20
  maxItems?: number;
21
21
  /** Seconds to cache. Defaults to 60 */
22
22
  revalidate?: number;
23
+ /**
24
+ * Visual layout for the project cards.
25
+ * - "list" (default) — image + text with border-bottom separator
26
+ * - "card" — full baseball-card style matching the ProjectPortfolio grid
27
+ */
28
+ variant?: "list" | "card";
29
+ /**
30
+ * Explicit list of project slugs to display, in order.
31
+ * When provided, overrides `filters` entirely — only these projects are shown.
32
+ * e.g. projectSlugs={["jacob-javits", "tillamook-bay-community-college"]}
33
+ */
34
+ projectSlugs?: string[];
23
35
  }
24
- export declare function SimilarProjects({ filters, excludeSlug, clientSlug, apiBase, basePath, maxItems, revalidate, }: SimilarProjectsProps): Promise<import("react/jsx-runtime").JSX.Element | null>;
36
+ export declare function SimilarProjects({ filters, excludeSlug, clientSlug, apiBase, basePath, maxItems, revalidate, variant, projectSlugs, }: SimilarProjectsProps): Promise<import("react/jsx-runtime").JSX.Element | null>;
25
37
  //# sourceMappingURL=SimilarProjects.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SimilarProjects.d.ts","sourceRoot":"","sources":["../src/SimilarProjects.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA8CD,wBAAsB,eAAe,CAAC,EACpC,OAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,QAAY,EACZ,UAAe,GAChB,EAAE,oBAAoB,2DA6GtB"}
1
+ {"version":3,"file":"SimilarProjects.d.ts","sourceRoot":"","sources":["../src/SimilarProjects.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AA6DD,wBAAsB,eAAe,CAAC,EACpC,OAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,QAAY,EACZ,UAAe,EACf,OAAgB,EAChB,YAAY,GACb,EAAE,oBAAoB,2DA8JtB"}